1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 07:37:30 +00:00

Resolve "Date lower and higher than filter"

This commit is contained in:
Sascha Jullmann 2021-07-08 16:20:06 +00:00
parent cfcdcd33e0
commit e55a28da65
6 changed files with 329 additions and 1 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/view
changelog.md
web-frontend/modules/database

View file

@ -93,6 +93,8 @@ class DatabaseConfig(AppConfig):
EmptyViewFilterType,
NotEmptyViewFilterType,
DateEqualViewFilterType,
DateBeforeViewFilterType,
DateAfterViewFilterType,
DateNotEqualViewFilterType,
DateEqualsTodayViewFilterType,
DateEqualsCurrentMonthViewFilterType,
@ -115,6 +117,8 @@ class DatabaseConfig(AppConfig):
view_filter_type_registry.register(HigherThanViewFilterType())
view_filter_type_registry.register(LowerThanViewFilterType())
view_filter_type_registry.register(DateEqualViewFilterType())
view_filter_type_registry.register(DateBeforeViewFilterType())
view_filter_type_registry.register(DateAfterViewFilterType())
view_filter_type_registry.register(DateNotEqualViewFilterType())
view_filter_type_registry.register(DateEqualsTodayViewFilterType())
view_filter_type_registry.register(DateEqualsCurrentMonthViewFilterType())

View file

@ -5,7 +5,7 @@ from math import floor, ceil
from dateutil import parser
from dateutil.parser import ParserError
from django.contrib.postgres.fields import JSONField
from django.db.models import Q, IntegerField, BooleanField
from django.db.models import Q, IntegerField, BooleanField, DateTimeField
from django.db.models.fields.related import ManyToManyField, ForeignKey
from pytz import timezone, all_timezones
@ -223,6 +223,83 @@ class DateEqualViewFilterType(ViewFilterType):
return Q(**{field_name: datetime})
class BaseDateFieldLookupFilterType(ViewFilterType):
"""
The base date field lookup filter serves as a base class for DateViewFilters.
With it a valid ISO date can be parsed into a date object which subsequently can
be used to filter a model.DateField or model.DateTimeField.
If the model field in question is a DateTimeField then the get_filter function
makes sure to only use the date part of the datetime in order to filter. This means
that the time part of a DateTimeField gets completely ignored.
The 'query_field_lookup' needs to be set on the deriving classes to something like
'__lt'
'__lte'
'__gt'
'__gte'
"""
type = "base_date_field_lookup_type"
query_field_lookup = ""
compatible_field_types = [DateFieldType.type]
@staticmethod
def parse_date(value: str) -> datetime.date:
"""
Parses the provided value string and converts it to a date object.
Raises an error if the provided value is an empty string or cannot be parsed
to a date object
"""
value = value.strip()
if value == "":
raise ValueError
try:
parsed_date = parser.isoparse(value).date()
return parsed_date
except ValueError as e:
raise e
def get_filter(self, field_name, value, model_field, field):
# in order to only compare the date part of a datetime field
# we need to verify that we are in fact dealing with a datetime field
# if so the django query lookup '__date' gets appended to the field_name
# otherwise (i.e. it is a date field) nothing gets appended
query_date_lookup = ""
if isinstance(model_field, DateTimeField):
query_date_lookup = "__date"
try:
parsed_date = self.parse_date(value)
field_key = f"{field_name}{query_date_lookup}{self.query_field_lookup}"
return Q(**{field_key: parsed_date})
except (ParserError, ValueError):
return Q()
class DateBeforeViewFilterType(BaseDateFieldLookupFilterType):
"""
The date before filter parses the provided filter value as date and checks if the
field value is before this date (lower than).
It is an extension of the BaseDateFieldLookupFilter
"""
type = "date_before"
query_field_lookup = "__lt"
compatible_field_types = [DateFieldType.type]
class DateAfterViewFilterType(BaseDateFieldLookupFilterType):
"""
The after date filter parses the provided filter value as date and checks if
the field value is after this date (greater than).
It is an extension of the BaseDateFieldLookupFilter
"""
type = "date_after"
query_field_lookup = "__gt"
class DateEqualsTodayViewFilterType(ViewFilterType):
"""
The today filter checks if the field value matches with today's date.

View file

@ -9,6 +9,7 @@ from django.utils.timezone import make_aware, datetime
from baserow.contrib.database.views.registries import view_filter_type_registry
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.views.view_filters import BaseDateFieldLookupFilterType
@pytest.mark.django_db
@ -1431,6 +1432,161 @@ def test_date_not_equal_filter_type(data_fixture):
assert len(ids) == 4
def test_date_parser_mixin():
date_parser = BaseDateFieldLookupFilterType()
date_string = "2021-07-05"
parsed_date = date_parser.parse_date(date_string)
assert parsed_date.year == 2021
assert parsed_date.month == 7
assert parsed_date.day == 5
date_string = " 2021-07-06 "
parsed_date = date_parser.parse_date(date_string)
assert parsed_date.year == 2021
assert parsed_date.month == 7
assert parsed_date.day == 6
date_string = ""
with pytest.raises(ValueError):
date_parser.parse_date(date_string)
date_string = "2021-15-15"
with pytest.raises(ValueError):
date_parser.parse_date(date_string)
@pytest.mark.django_db
def test_date_before_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)
date_field = data_fixture.create_date_field(table=table)
date_time_field = data_fixture.create_date_field(
table=table, date_include_time=True
)
handler = ViewHandler()
model = table.get_model()
utc = timezone("UTC")
row = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 7, 5),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 7, 5, 1, 30, 0), utc
),
}
)
row_2 = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 7, 6),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 7, 6, 1, 30, 5), utc
),
}
)
row_3 = model.objects.create(
**{f"field_{date_field.id}": None, f"field_{date_time_field.id}": None}
)
row_4 = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 8, 1),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 8, 1, 2, 45, 45), utc
),
}
)
filter = data_fixture.create_view_filter(
view=grid_view, field=date_field, type="date_before", value="2021-07-06"
)
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1
assert row.id in ids
filter.field = date_time_field
filter.value = "2021-07-06"
filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1
assert row.id in ids
filter.value = ""
filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 4
assert row.id in ids
assert row_2.id in ids
assert row_3.id in ids
assert row_4.id in ids
@pytest.mark.django_db
def test_date_after_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)
date_field = data_fixture.create_date_field(table=table)
date_time_field = data_fixture.create_date_field(
table=table, date_include_time=True
)
handler = ViewHandler()
model = table.get_model()
utc = timezone("UTC")
row = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 7, 5),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 7, 5, 1, 30, 0), utc
),
}
)
row_2 = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 7, 6),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 7, 6, 2, 40, 5), utc
),
}
)
row_3 = model.objects.create(
**{f"field_{date_field.id}": None, f"field_{date_time_field.id}": None}
)
row_4 = model.objects.create(
**{
f"field_{date_field.id}": date(2021, 8, 1),
f"field_{date_time_field.id}": make_aware(
datetime(2021, 8, 1, 2, 45, 45), utc
),
}
)
filter = data_fixture.create_view_filter(
view=grid_view, field=date_field, type="date_after", value="2021-07-06"
)
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
filter.field = date_time_field
filter.value = "2021-07-06"
filter.save()
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
filter.value = ""
filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 4
assert row.id in ids
assert row_2.id in ids
assert row_3.id in ids
assert row_4.id in ids
@pytest.mark.django_db
def test_empty_filter_type(data_fixture):
user = data_fixture.create_user()

View file

@ -5,6 +5,7 @@
* Made it possible to list table field meta-data with a token.
* Fix the create group invite endpoint failing when no message provided.
* Single select options can now be ordered by drag and drop.
* Added before and after date filters.
## Released (2021-06-02)

View file

@ -31,6 +31,8 @@ import {
DateEqualsTodayViewFilterType,
DateEqualsCurrentMonthViewFilterType,
DateEqualsCurrentYearViewFilterType,
DateBeforeViewFilterType,
DateAfterViewFilterType,
} from '@baserow/modules/database/viewFilters'
import {
CSVImporterType,
@ -70,6 +72,8 @@ export default ({ store, app }) => {
'viewFilter',
new DateEqualsCurrentYearViewFilterType()
)
app.$registry.register('viewFilter', new DateBeforeViewFilterType())
app.$registry.register('viewFilter', new DateAfterViewFilterType())
app.$registry.register('viewFilter', new ContainsViewFilterType())
app.$registry.register('viewFilter', new FilenameContainsViewFilterType())
app.$registry.register('viewFilter', new ContainsNotViewFilterType())

View file

@ -269,6 +269,92 @@ export class DateEqualViewFilterType extends ViewFilterType {
}
}
export class DateBeforeViewFilterType extends ViewFilterType {
static getType() {
return 'date_before'
}
getName() {
return 'before date'
}
getExample() {
return '2020-01-01'
}
getInputComponent() {
return ViewFilterTypeDate
}
getCompatibleFieldTypes() {
return ['date']
}
matches(rowValue, filterValue, field, fieldType) {
// parse the provided string values as moment objects in order to make
// date comparisons
const filterDate = moment.utc(filterValue, 'YYYY-MM-DD')
const rowDate = moment.utc(rowValue, 'YYYY-MM-DD')
// if the filter date is not a valid date we can immediately return
// true because without a valid date the filter won't be applied
if (!filterDate.isValid()) {
return true
}
// if the row value is null or the rowDate is not valid we can immediately return
// false since it does not match the filter and the row won't be in the resultset
if (rowValue === null || !rowDate.isValid()) {
return false
}
return rowDate.isBefore(filterDate)
}
}
export class DateAfterViewFilterType extends ViewFilterType {
static getType() {
return 'date_after'
}
getName() {
return 'after date'
}
getExample() {
return '2020-01-01'
}
getInputComponent() {
return ViewFilterTypeDate
}
getCompatibleFieldTypes() {
return ['date']
}
matches(rowValue, filterValue, field, fieldType) {
// parse the provided string values as moment objects in order to make
// date comparisons
const filterDate = moment.utc(filterValue, 'YYYY-MM-DD')
const rowDate = moment.utc(rowValue, 'YYYY-MM-DD')
// if the filter date is not a valid date we can immediately return
// true because without a valid date the filter won't be applied
if (!filterDate.isValid()) {
return true
}
// if the row value is null or the rowDate is not valid we can immediately return
// false since it does not match the filter and the row won't be in the resultset
if (rowValue === null || !rowDate.isValid()) {
return false
}
return rowDate.isAfter(filterDate)
}
}
export class DateNotEqualViewFilterType extends ViewFilterType {
static getType() {
return 'date_not_equal'