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:
parent
cfcdcd33e0
commit
e55a28da65
6 changed files with 329 additions and 1 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/view
web-frontend/modules/database
|
@ -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())
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue