1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-05-13 21:01:43 +00:00

Resolve "Last modified field"

This commit is contained in:
Sascha Jullmann 2021-08-11 07:39:58 +00:00
parent f574d7c145
commit 4696b98ac4
40 changed files with 2760 additions and 188 deletions

View file

@ -63,6 +63,8 @@ class DatabaseConfig(AppConfig):
NumberFieldType,
BooleanFieldType,
DateFieldType,
LastModifiedFieldType,
CreatedOnFieldType,
LinkRowFieldType,
EmailFieldType,
FileFieldType,
@ -77,6 +79,8 @@ class DatabaseConfig(AppConfig):
field_type_registry.register(NumberFieldType())
field_type_registry.register(BooleanFieldType())
field_type_registry.register(DateFieldType())
field_type_registry.register(LastModifiedFieldType())
field_type_registry.register(CreatedOnFieldType())
field_type_registry.register(LinkRowFieldType())
field_type_registry.register(FileFieldType())
field_type_registry.register(SingleSelectFieldType())

View file

@ -42,6 +42,58 @@ def construct_all_possible_field_kwargs(
{"name": "datetime_eu", "date_include_time": True, "date_format": "EU"},
{"name": "date_eu", "date_include_time": False, "date_format": "EU"},
],
"last_modified": [
{
"name": "last_modified_datetime_us",
"date_include_time": True,
"date_format": "US",
"timezone": "Europe/Berlin",
},
{
"name": "last_modified_date_us",
"date_include_time": False,
"date_format": "US",
"timezone": "Europe/Berlin",
},
{
"name": "last_modified_datetime_eu",
"date_include_time": True,
"date_format": "EU",
"timezone": "Europe/Berlin",
},
{
"name": "last_modified_date_eu",
"date_include_time": False,
"date_format": "EU",
"timezone": "Europe/Berlin",
},
],
"created_on": [
{
"name": "created_on_datetime_us",
"date_include_time": True,
"date_format": "US",
"timezone": "Europe/Berlin",
},
{
"name": "created_on_date_us",
"date_include_time": False,
"date_format": "US",
"timezone": "Europe/Berlin",
},
{
"name": "created_on_datetime_eu",
"date_include_time": True,
"date_format": "EU",
"timezone": "Europe/Berlin",
},
{
"name": "created_on_date_eu",
"date_include_time": False,
"date_format": "EU",
"timezone": "Europe/Berlin",
},
],
"link_row": [
{"name": "link_row", "link_row_table": link_table},
{"name": "decimal_link_row", "link_row_table": decimal_link_table},

View file

@ -1,3 +1,4 @@
import pytz
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime, date
@ -50,6 +51,8 @@ from .models import (
NumberField,
BooleanField,
DateField,
LastModifiedField,
CreatedOnField,
LinkRowField,
EmailField,
FileField,
@ -544,6 +547,157 @@ class DateFieldType(FieldType):
setattr(row, field_name, datetime.fromisoformat(value))
class CreatedOnLastModifiedBaseFieldType(DateFieldType):
can_be_in_form_view = False
allowed_fields = DateFieldType.allowed_fields + ["timezone"]
serializer_field_names = DateFieldType.serializer_field_names + ["timezone"]
serializer_field_overrides = {
"timezone": serializers.ChoiceField(choices=pytz.all_timezones, required=True)
}
source_field_name = None
model_field_kwargs = {}
def prepare_value_for_db(self, instance, value):
"""
Since the LastModified and CreatedOnFieldTypes are read only fields, we raise a
ValidationError when there is a value present.
"""
if not value:
return value
raise ValidationError(
f"Field of type {self.type} is read only and should not be set manually."
)
def get_export_value(self, value, field_object):
if value is None:
return value
python_format = field_object["field"].get_python_format()
field = field_object["field"]
field_timezone = timezone(field.get_timezone())
return value.astimezone(field_timezone).strftime(python_format)
def get_serializer_field(self, instance, **kwargs):
if not instance.date_include_time:
kwargs["format"] = "%Y-%m-%d"
kwargs["default_timezone"] = timezone(instance.timezone)
return serializers.DateTimeField(
**{
"required": False,
**kwargs,
}
)
def get_model_field(self, instance, **kwargs):
kwargs["null"] = True
kwargs["blank"] = True
kwargs.update(self.model_field_kwargs)
return models.DateTimeField(**kwargs)
def contains_query(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()
return AnnotatedQ(
annotation={
f"formatted_date_{field_name}": Coalesce(
RawSQL(
f"""TO_CHAR({field_name} at time zone %s,
'{field.get_psql_format()}')""",
[field.get_timezone()],
output_field=CharField(),
),
Value(""),
)
},
q={f"formatted_date_{field_name}__icontains": value},
)
def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
"""
If the field type has changed then we want to convert the date or timestamp to
a human readable text following the old date format.
"""
to_field_type = field_type_registry.get_by_model(to_field)
if to_field_type.type != self.type:
sql_format = from_field.get_psql_format()
variables = {}
variable_name = f"{from_field.db_column}_timezone"
variables[variable_name] = from_field.get_timezone()
return (
f"""p_in = TO_CHAR(p_in::timestamptz at time zone %({variable_name})s,
'{sql_format}');""",
variables,
)
return super().get_alter_column_prepare_old_value(
connection, from_field, to_field
)
def after_create(self, field, model, user, connection, before):
"""
Immediately after the field has been created, we need to populate the values
with the already existing source_field_name column.
"""
model.objects.all().update(
**{f"{field.db_column}": models.F(self.source_field_name)}
)
def after_update(
self,
from_field,
to_field,
from_model,
to_model,
user,
connection,
altered_column,
before,
):
"""
If the field type has changed, we need to update the values from the from
the source_field_name column.
"""
if not isinstance(from_field, self.model_class):
to_model.objects.all().update(
**{f"{to_field.db_column}": models.F(self.source_field_name)}
)
def get_export_serialized_value(self, row, field_name, cache, files_zip, storage):
return None
def set_import_serialized_value(
self, row, field_name, value, id_mapping, files_zip, storage
):
"""
We don't want to do anything here because we don't have the right value yet
and it will automatically be set when the row is saved.
"""
def random_value(self, instance, fake, cache):
return getattr(instance, self.source_field_name)
class LastModifiedFieldType(CreatedOnLastModifiedBaseFieldType):
type = "last_modified"
model_class = LastModifiedField
source_field_name = "updated_on"
model_field_kwargs = {"auto_now": True}
class CreatedOnFieldType(CreatedOnLastModifiedBaseFieldType):
type = "created_on"
model_class = CreatedOnField
source_field_name = "created_on"
model_field_kwargs = {"auto_now_add": True}
class LinkRowFieldType(FieldType):
"""
The link row field can be used to link a field to a row of another table. Because

View file

@ -0,0 +1,112 @@
from django.db import models
import pytz
DATE_FORMAT = {
"EU": {"name": "European (D/M/Y)", "format": "%d/%m/%Y", "sql": "DD/MM/YYYY"},
"US": {"name": "US (M/D/Y)", "format": "%m/%d/%Y", "sql": "MM/DD/YYYY"},
"ISO": {"name": "ISO (Y-M-D)", "format": "%Y-%m-%d", "sql": "YYYY-MM-DD"},
}
DATE_FORMAT_CHOICES = [(k, v["name"]) for k, v in DATE_FORMAT.items()]
DATE_TIME_FORMAT = {
"24": {"name": "24 hour", "format": "%H:%M", "sql": "HH24:MI"},
"12": {"name": "12 hour", "format": "%I:%M %p", "sql": "HH12:MIAM"},
}
DATE_TIME_FORMAT_CHOICES = [(k, v["name"]) for k, v in DATE_TIME_FORMAT.items()]
class BaseDateMixin(models.Model):
date_format = models.CharField(
choices=DATE_FORMAT_CHOICES,
default=DATE_FORMAT_CHOICES[0][0],
max_length=32,
help_text="EU (20/02/2020), US (02/20/2020) or ISO (2020-02-20)",
)
date_include_time = models.BooleanField(
default=False, help_text="Indicates if the field also includes a time."
)
date_time_format = models.CharField(
choices=DATE_TIME_FORMAT_CHOICES,
default=DATE_TIME_FORMAT_CHOICES[0][0],
max_length=32,
help_text="24 (14:30) or 12 (02:30 PM)",
)
class Meta:
abstract = True
def get_python_format(self):
"""
Returns the strftime format as a string based on the field's properties. This
could for example be '%Y-%m-%d %H:%I'.
:return: The strftime format based on the field's properties.
:rtype: str
"""
return self._get_format("format")
def get_psql_format(self):
"""
Returns the sql datetime format as a string based on the field's properties.
This could for example be 'YYYY-MM-DD HH12:MIAM'.
:return: The sql datetime format based on the field's properties.
:rtype: str
"""
return self._get_format("sql")
def get_psql_type(self):
"""
Returns the postgresql column type used by this field depending on if it is a
date or datetime.
:return: The postgresql column type either 'timestamp' or 'date'
:rtype: str
"""
return "timestamp" if self.date_include_time else "date"
def get_psql_type_convert_function(self):
"""
Returns the postgresql function that can be used to coerce another postgresql
type to the correct type used by this field.
:return: The postgresql type conversion function, either 'TO_TIMESTAMP' or
'TO_DATE'
:rtype: str
"""
return "TO_TIMESTAMP" if self.date_include_time else "TO_DATE"
def _get_format(self, format_type):
date_format = DATE_FORMAT[self.date_format][format_type]
time_format = DATE_TIME_FORMAT[self.date_time_format][format_type]
if self.date_include_time:
return f"{date_format} {time_format}"
else:
return date_format
class TimezoneMixin(models.Model):
timezone = models.CharField(
max_length=255,
blank=False,
help_text="Timezone of User during field creation.",
default="UTC",
)
def get_timezone(self, fallback="UTC"):
return self.timezone if self.timezone in pytz.all_timezones else fallback
class Meta:
abstract = True
def save(self, *args, **kwargs):
"""Check if the timezone is a valid choice."""
if self.timezone not in pytz.all_timezones:
raise ValueError(f"{self.timezone} is not a valid choice.")
super().save(*args, **kwargs)

View file

@ -8,6 +8,7 @@ from baserow.core.mixins import (
CreatedAndUpdatedOnMixin,
TrashableModelMixin,
)
from baserow.contrib.database.fields.mixins import BaseDateMixin, TimezoneMixin
from baserow.core.utils import to_snake_case, remove_special_characters
NUMBER_TYPE_INTEGER = "INTEGER"
@ -25,19 +26,6 @@ NUMBER_DECIMAL_PLACES_CHOICES = (
(5, "1.00000"),
)
DATE_FORMAT = {
"EU": {"name": "European (D/M/Y)", "format": "%d/%m/%Y", "sql": "DD/MM/YYYY"},
"US": {"name": "US (M/D/Y)", "format": "%m/%d/%Y", "sql": "MM/DD/YYYY"},
"ISO": {"name": "ISO (Y-M-D)", "format": "%Y-%m-%d", "sql": "YYYY-MM-DD"},
}
DATE_FORMAT_CHOICES = [(k, v["name"]) for k, v in DATE_FORMAT.items()]
DATE_TIME_FORMAT = {
"24": {"name": "24 hour", "format": "%H:%M", "sql": "HH24:MI"},
"12": {"name": "12 hour", "format": "%I:%M %p", "sql": "HH12:MIAM"},
}
DATE_TIME_FORMAT_CHOICES = [(k, v["name"]) for k, v in DATE_TIME_FORMAT.items()]
def get_default_field_content_type():
return ContentType.objects.get_for_model(Field)
@ -170,75 +158,16 @@ class BooleanField(Field):
pass
class DateField(Field):
date_format = models.CharField(
choices=DATE_FORMAT_CHOICES,
default=DATE_FORMAT_CHOICES[0][0],
max_length=32,
help_text="EU (20/02/2020), US (02/20/2020) or ISO (2020-02-20)",
)
date_include_time = models.BooleanField(
default=False, help_text="Indicates if the field also includes a time."
)
date_time_format = models.CharField(
choices=DATE_TIME_FORMAT_CHOICES,
default=DATE_TIME_FORMAT_CHOICES[0][0],
max_length=32,
help_text="24 (14:30) or 12 (02:30 PM)",
)
class DateField(Field, BaseDateMixin):
pass
def get_python_format(self):
"""
Returns the strftime format as a string based on the field's properties. This
could for example be '%Y-%m-%d %H:%I'.
:return: The strftime format based on the field's properties.
:rtype: str
"""
class LastModifiedField(Field, BaseDateMixin, TimezoneMixin):
pass
return self._get_format("format")
def get_psql_format(self):
"""
Returns the sql datetime format as a string based on the field's properties.
This could for example be 'YYYY-MM-DD HH12:MIAM'.
:return: The sql datetime format based on the field's properties.
:rtype: str
"""
return self._get_format("sql")
def get_psql_type(self):
"""
Returns the postgresql column type used by this field depending on if it is a
date or datetime.
:return: The postgresql column type either 'timestamp' or 'date'
:rtype: str
"""
return "timestamp" if self.date_include_time else "date"
def get_psql_type_convert_function(self):
"""
Returns the postgresql function that can be used to coerce another postgresql
type to the correct type used by this field.
:return: The postgresql type conversion function, either 'TO_TIMESTAMP' or
'TO_DATE'
:rtype: str
"""
return "TO_TIMESTAMP" if self.date_include_time else "TO_DATE"
def _get_format(self, format_type):
date_format = DATE_FORMAT[self.date_format][format_type]
time_format = DATE_TIME_FORMAT[self.date_time_format][format_type]
if self.date_include_time:
return f"{date_format} {time_format}"
else:
return date_format
class CreatedOnField(Field, BaseDateMixin, TimezoneMixin):
pass
class LinkRowField(Field):

View file

@ -0,0 +1,128 @@
# Generated by Django 2.2.24 on 2021-08-09 15:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("database", "0035_remove_field_old_name"),
]
operations = [
migrations.CreateModel(
name="CreatedOnField",
fields=[
(
"field_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="database.Field",
),
),
(
"date_format",
models.CharField(
choices=[
("EU", "European (D/M/Y)"),
("US", "US (M/D/Y)"),
("ISO", "ISO (Y-M-D)"),
],
default="EU",
help_text="EU (20/02/2020), US (02/20/2020) or ISO (2020-02-20)", # noqa: E501
max_length=32,
),
),
(
"date_include_time",
models.BooleanField(
default=False,
help_text="Indicates if the field also includes a time.",
),
),
(
"date_time_format",
models.CharField(
choices=[("24", "24 hour"), ("12", "12 hour")],
default="24",
help_text="24 (14:30) or 12 (02:30 PM)",
max_length=32,
),
),
(
"timezone",
models.CharField(
default="UTC",
help_text="Timezone of User during field creation.",
max_length=255,
),
),
],
options={
"abstract": False,
},
bases=("database.field", models.Model),
),
migrations.CreateModel(
name="LastModifiedField",
fields=[
(
"field_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="database.Field",
),
),
(
"date_format",
models.CharField(
choices=[
("EU", "European (D/M/Y)"),
("US", "US (M/D/Y)"),
("ISO", "ISO (Y-M-D)"),
],
default="EU",
help_text="EU (20/02/2020), US (02/20/2020) or ISO (2020-02-20)", # noqa: E501
max_length=32,
),
),
(
"date_include_time",
models.BooleanField(
default=False,
help_text="Indicates if the field also includes a time.",
),
),
(
"date_time_format",
models.CharField(
choices=[("24", "24 hour"), ("12", "12 hour")],
default="24",
help_text="24 (14:30) or 12 (02:30 PM)",
max_length=32,
),
),
(
"timezone",
models.CharField(
default="UTC",
help_text="Timezone of User during field creation.",
max_length=255,
),
),
],
options={
"abstract": False,
},
bases=("database.field", models.Model),
),
]

View file

@ -758,6 +758,7 @@ class ViewHandler:
if model is None:
model = view.table.get_model()
queryset = model.objects.all().enhance_by_fields()
view_type = view_type_registry.get_by_model(view.specific_class)

View file

@ -9,16 +9,19 @@ from django.db.models import Q, IntegerField, BooleanField, DateTimeField
from django.db.models.fields.related import ManyToManyField, ForeignKey
from pytz import timezone, all_timezones
from baserow.contrib.database.fields.field_filters import AnnotatedQ
from baserow.contrib.database.fields.field_filters import (
filename_contains_filter,
OptionallyAnnotatedQ,
)
from baserow.contrib.database.fields.field_types import (
CreatedOnFieldType,
TextFieldType,
LongTextFieldType,
URLFieldType,
NumberFieldType,
DateFieldType,
LastModifiedFieldType,
LinkRowFieldType,
BooleanFieldType,
EmailFieldType,
@ -27,6 +30,7 @@ from baserow.contrib.database.fields.field_types import (
PhoneNumberFieldType,
)
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.core.expressions import Timezone
from .registries import ViewFilterType
@ -104,6 +108,8 @@ class ContainsViewFilterType(ViewFilterType):
EmailFieldType.type,
PhoneNumberFieldType.type,
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
SingleSelectFieldType.type,
NumberFieldType.type,
]
@ -187,7 +193,11 @@ class DateEqualViewFilterType(ViewFilterType):
"""
type = "date_equal"
compatible_field_types = [DateFieldType.type]
compatible_field_types = [
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
]
def get_filter(self, field_name, value, model_field, field):
"""
@ -207,18 +217,34 @@ class DateEqualViewFilterType(ViewFilterType):
except (ParserError, ValueError):
return Q()
# If the length if string value is lower than 10 characters we know it is only
# a date so we can match only on year, month and day level. This way if a date
# is provided, but if it tries to compare with a models.DateTimeField it will
# still give back accurate results.
# If the length of the string value is lower than 10 characters we know it is
# only a date so we can match only on year, month and day level. This way if a
# date is provided, but if it tries to compare with a models.DateTimeField it
# will still give back accurate results.
# Since the LastModified and CreateOn fields are stored for a specific timezone
# we need to make sure to take this timezone into account when comparing to
# the "equals_date"
has_timezone = hasattr(field, "timezone")
if len(value) <= 10:
return Q(
**{
f"{field_name}__year": datetime.year,
f"{field_name}__month": datetime.month,
f"{field_name}__day": datetime.day,
def query_dict(query_field_name):
return {
f"{query_field_name}__year": datetime.year,
f"{query_field_name}__month": datetime.month,
f"{query_field_name}__day": datetime.day,
}
)
if has_timezone:
timezone_string = field.get_timezone()
tmp_field_name = f"{field_name}_timezone_{timezone_string}"
return AnnotatedQ(
annotation={
f"{tmp_field_name}": Timezone(field_name, timezone_string)
},
q=query_dict(tmp_field_name),
)
else:
return Q(**query_dict(field_name))
else:
return Q(**{field_name: datetime})
@ -241,7 +267,11 @@ class BaseDateFieldLookupFilterType(ViewFilterType):
type = "base_date_field_lookup_type"
query_field_lookup = ""
compatible_field_types = [DateFieldType.type]
compatible_field_types = [
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
]
@staticmethod
def parse_date(value: str) -> datetime.date:
@ -271,8 +301,24 @@ class BaseDateFieldLookupFilterType(ViewFilterType):
query_date_lookup = "__date"
try:
parsed_date = self.parse_date(value)
has_timezone = hasattr(field, "timezone")
field_key = f"{field_name}{query_date_lookup}{self.query_field_lookup}"
return Q(**{field_key: parsed_date})
if has_timezone:
timezone_string = field.get_timezone()
tmp_field_name = f"{field_name}_timezone_{timezone_string}"
field_key = (
f"{tmp_field_name}{query_date_lookup}{self.query_field_lookup}"
)
return AnnotatedQ(
annotation={
f"{tmp_field_name}": Timezone(field_name, timezone_string)
},
q={field_key: parsed_date},
)
else:
return Q(**{field_key: parsed_date})
except (ParserError, ValueError):
return Q()
@ -286,7 +332,11 @@ class DateBeforeViewFilterType(BaseDateFieldLookupFilterType):
type = "date_before"
query_field_lookup = "__lt"
compatible_field_types = [DateFieldType.type]
compatible_field_types = [
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
]
class DateAfterViewFilterType(BaseDateFieldLookupFilterType):
@ -306,21 +356,38 @@ class DateEqualsTodayViewFilterType(ViewFilterType):
"""
type = "date_equals_today"
compatible_field_types = [DateFieldType.type]
compatible_field_types = [
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
]
query_for = ["year", "month", "day"]
def get_filter(self, field_name, value, model_field, field):
timezone_string = value if value in all_timezones else "UTC"
timezone_object = timezone(timezone_string)
field_has_timezone = hasattr(field, "timezone")
now = datetime.utcnow().astimezone(timezone_object)
query_dict = dict()
if "year" in self.query_for:
query_dict[f"{field_name}__year"] = now.year
if "month" in self.query_for:
query_dict[f"{field_name}__month"] = now.month
if "day" in self.query_for:
query_dict[f"{field_name}__day"] = now.day
return Q(**query_dict)
def make_query_dict(query_field_name):
query_dict = dict()
if "year" in self.query_for:
query_dict[f"{query_field_name}__year"] = now.year
if "month" in self.query_for:
query_dict[f"{query_field_name}__month"] = now.month
if "day" in self.query_for:
query_dict[f"{query_field_name}__day"] = now.day
return query_dict
if field_has_timezone:
tmp_field_name = f"{field_name}_timezone_{timezone_string}"
return AnnotatedQ(
annotation={f"{tmp_field_name}": Timezone(field_name, timezone_string)},
q=make_query_dict(tmp_field_name),
)
else:
return Q(**make_query_dict(field_name))
class DateEqualsCurrentMonthViewFilterType(DateEqualsTodayViewFilterType):
@ -489,6 +556,8 @@ class EmptyViewFilterType(ViewFilterType):
NumberFieldType.type,
BooleanFieldType.type,
DateFieldType.type,
LastModifiedFieldType.type,
CreatedOnFieldType.type,
LinkRowFieldType.type,
EmailFieldType.type,
FileFieldType.type,

View file

@ -0,0 +1,46 @@
from django.db.models import Expression, DateTimeField, Value
class Timezone(Expression):
"""
This expression can convert an existing datetime value to another timezone. It
can for example by used like this:
```
SomeModel.objects.all().annotate(
created_on_in_amsterdam=Timezone("created_on", "Europe/Amsterdam")
).filter(created_on_in_amsterdam__day=1)
```
It will eventually result in `created_on at time zone 'Europe/Amsterdam'`
"""
def __init__(self, expression, timezone):
super().__init__(output_field=DateTimeField())
self.source_expression = self._parse_expressions(expression)[0]
self.timezone = timezone
def resolve_expression(
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
c = self.copy()
c.is_summary = summarize
c.source_expression = self.source_expression.resolve_expression(
query, allow_joins, reuse, summarize, for_save
)
return c
def __repr__(self):
return "{}({}, {})".format(
self.__class__.__name__,
self.source_expression,
self.timezone,
)
def as_sql(self, compiler, connection):
params = []
field_sql, field_params = compiler.compile(self.source_expression)
timezone_sql, timezone_params = compiler.compile(Value(self.timezone))
params.extend(field_params)
params.extend(timezone_params)
return f"{field_sql} at time zone {timezone_sql}", params

View file

@ -9,6 +9,8 @@ from pytz import timezone
from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST
from baserow.contrib.database.fields.models import (
CreatedOnField,
LastModifiedField,
LongTextField,
URLField,
DateField,
@ -886,3 +888,184 @@ def test_phone_number_field_type(api_client, data_fixture):
response = api_client.delete(email, HTTP_AUTHORIZATION=f"JWT {token}")
assert response.status_code == HTTP_204_NO_CONTENT
assert PhoneNumberField.objects.all().count() == 0
@pytest.mark.django_db
def test_last_modified_field_type(api_client, data_fixture):
time_under_test = "2021-08-10 12:00"
with freeze_time(time_under_test):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table = data_fixture.create_database_table(user=user)
# first add text field so that there is already a row with an
# updated_on value
text_field = data_fixture.create_text_field(user=user, table=table)
with freeze_time(time_under_test):
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{f"field_{text_field.id}": "Test Text"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
# now add a last_modified field with datetime
with freeze_time(time_under_test):
response = api_client.post(
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
{
"name": "Last",
"type": "last_modified",
"date_include_time": True,
"timezone": "Europe/Berlin",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["type"] == "last_modified"
assert LastModifiedField.objects.all().count() == 1
last_modified_field_id = response_json["id"]
assert last_modified_field_id
# verify that the timestamp is the same as the updated_on column
model = table.get_model(attribute_names=True)
row = model.objects.all().last()
assert row.last == row.updated_on
# change the text_field value so that we can verify that the
# last_modified column gets updated as well
with freeze_time(time_under_test):
response = api_client.patch(
reverse(
"api:database:rows:item",
kwargs={"table_id": table.id, "row_id": row.id},
),
{f"field_{text_field.id}": "test_second"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
last_datetime = row.last
updated_on_datetime = row.updated_on
assert last_datetime == updated_on_datetime
with freeze_time(time_under_test):
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{
f"field_{last_modified_field_id}": "2021-08-05",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
with freeze_time(time_under_test):
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{
f"field_{last_modified_field_id}": "2021-08-09T14:14:33.574356Z",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
@pytest.mark.django_db
def test_created_on_field_type(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1"
)
table = data_fixture.create_database_table(user=user)
# first add text field so that there is already a row with an
# updated_on and a created_on value
text_field = data_fixture.create_text_field(user=user, table=table)
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{f"field_{text_field.id}": "Test Text"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
# now add a created_on field with datetime
response = api_client.post(
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
{
"name": "Create",
"type": "created_on",
"date_include_time": True,
"timezone": "Europe/Berlin",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["type"] == "created_on"
assert CreatedOnField.objects.all().count() == 1
created_on_field_id = response_json["id"]
assert created_on_field_id
# verify that the timestamp is the same as the updated_on column
model = table.get_model(attribute_names=True)
row = model.objects.all().last()
assert row.create == row.created_on
# change the text_field value so that we can verify that the
# created_on column does NOT get updated
response = api_client.patch(
reverse(
"api:database:rows:item",
kwargs={"table_id": table.id, "row_id": row.id},
),
{f"field_{text_field.id}": "test_second"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
row = model.objects.all().last()
create_datetime = row.create
created_on_datetime = row.created_on
assert create_datetime == created_on_datetime
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{
f"field_{created_on_field_id}": "2021-08-05",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
response = api_client.post(
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
{
f"field_{created_on_field_id}": "2021-08-09T14:14:33.574356Z",
},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"

View file

@ -221,6 +221,14 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
"date_us": "2020-02-01",
"datetime_eu": "2020-02-01T01:23:00Z",
"datetime_us": "2020-02-01T01:23:00Z",
"last_modified_date_eu": "2021-01-02",
"last_modified_date_us": "2021-01-02",
"last_modified_datetime_eu": "2021-01-02T12:00:00Z",
"last_modified_datetime_us": "2021-01-02T12:00:00Z",
"created_on_date_eu": "2021-01-02",
"created_on_date_us": "2021-01-02",
"created_on_datetime_eu": "2021-01-02T12:00:00Z",
"created_on_datetime_us": "2021-01-02T12:00:00Z",
"decimal_link_row": [
{"id": 1, "value": "1.234"},
{"id": 2, "value": "-123.456"},

View file

@ -575,6 +575,7 @@ def test_create_row(api_client, data_fixture):
assert getattr(row_4, f"field_{text_field_2.id}") == ""
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
response = api_client.post(
f"{url}?user_field_names=true",
{

View file

@ -217,11 +217,16 @@ def test_can_export_every_interesting_different_field_to_csv(
expected = (
"\ufeffid,text,long_text,url,email,negative_int,positive_int,"
"negative_decimal,positive_decimal,boolean,datetime_us,date_us,datetime_eu,"
"date_eu,link_row,decimal_link_row,file_link_row,file,single_select,"
"phone_number\r\n"
"1,,,,,,,,,False,,,,,,,,,,\r\n"
"date_eu,last_modified_datetime_us,last_modified_date_us,"
"last_modified_datetime_eu,last_modified_date_eu,created_on_datetime_us,"
"created_on_date_us,created_on_datetime_eu,created_on_date_eu,link_row,"
"decimal_link_row,file_link_row,file,single_select,phone_number\r\n"
"1,,,,,,,,,False,,,,,01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
"01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,,,,,,\r\n"
"2,text,long_text,https://www.google.com,test@example.com,-1,1,-1.2,1.2,True,"
"02/01/2020 01:23,02/01/2020,01/02/2020 01:23,01/02/2020,"
"01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
"01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
'"linked_row_1,linked_row_2,unnamed row 3","1.234,-123.456,unnamed row 3",'
'"visible_name=name.txt url=http://localhost:8000/media/user_files/test_hash'
'.txt,unnamed row 2",'

View file

@ -0,0 +1,221 @@
import pytest
from pytz import timezone
from datetime import datetime
from freezegun import freeze_time
from io import BytesIO
from django.core.exceptions import ValidationError
from baserow.core.handler import CoreHandler
from baserow.contrib.database.fields.models import CreatedOnField
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.rows.handler import RowHandler
@pytest.mark.django_db
def test_created_on_field_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_handler = FieldHandler()
row_handler = RowHandler()
timezone_to_test = "Europe/Berlin"
timezone_of_field = timezone(timezone_to_test)
time_to_freeze = "2021-08-10 12:00"
data_fixture.create_text_field(table=table, name="text_field", primary=True)
created_on_field_date = field_handler.create_field(
user=user,
table=table,
type_name="created_on",
name="Create Date",
timezone=timezone_to_test,
)
created_on_field_datetime = field_handler.create_field(
user=user,
table=table,
type_name="created_on",
name="Create Datetime",
date_include_time=True,
timezone=timezone_to_test,
)
assert created_on_field_date.date_include_time is False
assert created_on_field_datetime.date_include_time is True
assert len(CreatedOnField.objects.all()) == 2
model = table.get_model(attribute_names=True)
with pytest.raises(ValidationError):
row_handler.create_row(
user=user, table=table, values={created_on_field_date.id: "2021-08-09"}
)
with pytest.raises(ValidationError):
row_handler.create_row(
user=user,
table=table,
values={created_on_field_datetime.id: "2021-08-09T14:14:33.574356Z"},
)
with freeze_time(time_to_freeze):
row = row_handler.create_row(user=user, table=table, values={}, model=model)
assert row.create_date is not None
assert row.create_date == row.created_on
assert row.create_date is not None
row_create_datetime = row.create_datetime
row_created_on = row.created_on
assert row_create_datetime == row_created_on
# Trying to update the the created_on field will raise error
with pytest.raises(ValidationError):
row_handler.update_row(
user=user,
row_id=row.id,
table=table,
values={created_on_field_date.id: "2021-08-09"},
)
with pytest.raises(ValidationError):
row_handler.update_row(
user=user,
table=table,
row_id=row.id,
values={created_on_field_datetime.id: "2021-08-09T14:14:33.574356Z"},
)
# Updating the text field will NOT updated
# the created_on field.
row_create_datetime_before_update = row.create_datetime
row_create_date_before_update = row.create_date
row_handler.update_row(
user=user,
table=table,
row_id=row.id,
values={
"text_field": "Hello Test",
},
model=model,
)
row.refresh_from_db()
assert row.create_datetime == row_create_datetime_before_update
assert row.create_date == row_create_date_before_update
row_create_datetime_before_alter = row.create_datetime
# changing the field from CreatedOn to Datetime should persist the date
# in the corresponding timezone
with freeze_time(time_to_freeze):
field_handler.update_field(
user=user,
field=created_on_field_datetime,
new_type_name="date",
date_include_time=True,
)
assert len(CreatedOnField.objects.all()) == 1
row.refresh_from_db()
field_before_with_timezone = row_create_datetime_before_alter.astimezone(
timezone_of_field
)
assert row.create_datetime.year == field_before_with_timezone.year
assert row.create_datetime.month == field_before_with_timezone.month
assert row.create_datetime.day == field_before_with_timezone.day
assert row.create_datetime.hour == field_before_with_timezone.hour
assert row.create_datetime.minute == field_before_with_timezone.minute
assert row.create_datetime.second == field_before_with_timezone.second
# changing the field from LastModified with Datetime to Text Field should persist
# the datetime as string
field_handler.update_field(
user=user,
field=created_on_field_datetime,
new_type_name="created_on",
date_include_time=True,
timezone="Europe/Berlin",
)
assert len(CreatedOnField.objects.all()) == 2
row.refresh_from_db()
row_create_datetime_before_alter = row.create_datetime
field_handler.update_field(
user=user,
field=created_on_field_datetime,
new_type_name="text",
)
row.refresh_from_db()
assert len(CreatedOnField.objects.all()) == 1
assert row.create_datetime == row_create_datetime_before_alter.astimezone(
timezone_of_field
).strftime("%d/%m/%Y %H:%M")
# deleting the fields
field_handler.delete_field(user=user, field=created_on_field_date)
assert len(CreatedOnField.objects.all()) == 0
@pytest.mark.django_db
def test_created_on_field_type_wrong_timezone(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_handler = FieldHandler()
with pytest.raises(ValueError):
field_handler.create_field(
user=user,
table=table,
type_name="created_on",
name="Create Date",
timezone="SDj",
)
@pytest.mark.django_db
def test_import_export_last_modified_field(data_fixture):
user = data_fixture.create_user()
imported_group = data_fixture.create_group(user=user)
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
field_handler = FieldHandler()
created_on_field = field_handler.create_field(
user=user,
table=table,
name="Created On",
type_name="created_on",
)
row_handler = RowHandler()
with freeze_time("2020-01-01 12:00"):
row = row_handler.create_row(
user=user,
table=table,
values={},
)
assert getattr(row, f"field_{created_on_field.id}") == datetime(
2020, 1, 1, 12, 00, tzinfo=timezone("UTC")
)
core_handler = CoreHandler()
exported_applications = core_handler.export_group_applications(
database.group, BytesIO()
)
with freeze_time("2020-01-02 12:00"):
imported_applications, id_mapping = core_handler.import_applications_to_group(
imported_group, exported_applications, BytesIO(), None
)
imported_database = imported_applications[0]
imported_tables = imported_database.table_set.all()
imported_table = imported_tables[0]
import_created_on_field = imported_table.field_set.all().first().specific
imported_row = row_handler.get_row(user=user, table=imported_table, row_id=row.id)
assert imported_row.id == row.id
assert getattr(imported_row, f"field_{import_created_on_field.id}") == datetime(
2020, 1, 2, 12, 00, tzinfo=timezone("UTC")
)

View file

@ -486,6 +486,14 @@ def test_human_readable_values(data_fixture):
"date_us": "",
"datetime_eu": "",
"datetime_us": "",
"last_modified_date_eu": "02/01/2021",
"last_modified_date_us": "01/02/2021",
"last_modified_datetime_eu": "02/01/2021 13:00",
"last_modified_datetime_us": "01/02/2021 13:00",
"created_on_date_eu": "02/01/2021",
"created_on_date_us": "01/02/2021",
"created_on_datetime_eu": "02/01/2021 13:00",
"created_on_datetime_us": "01/02/2021 13:00",
"decimal_link_row": "",
"email": "",
"file": "",
@ -507,6 +515,14 @@ def test_human_readable_values(data_fixture):
"date_us": "02/01/2020",
"datetime_eu": "01/02/2020 01:23",
"datetime_us": "02/01/2020 01:23",
"last_modified_date_eu": "02/01/2021",
"last_modified_date_us": "01/02/2021",
"last_modified_datetime_eu": "02/01/2021 13:00",
"last_modified_datetime_us": "01/02/2021 13:00",
"created_on_date_eu": "02/01/2021",
"created_on_date_us": "01/02/2021",
"created_on_datetime_eu": "02/01/2021 13:00",
"created_on_datetime_us": "01/02/2021 13:00",
"decimal_link_row": "1.234, -123.456, unnamed row 3",
"email": "test@example.com",
"file": "a.txt, b.txt",

View file

@ -0,0 +1,225 @@
import pytest
from pytz import timezone
from datetime import datetime
from freezegun import freeze_time
from io import BytesIO
from django.core.exceptions import ValidationError
from baserow.core.handler import CoreHandler
from baserow.contrib.database.fields.models import LastModifiedField
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.rows.handler import RowHandler
@pytest.mark.django_db
def test_last_modified_field_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_handler = FieldHandler()
row_handler = RowHandler()
timezone_to_test = "Europe/Berlin"
timezone_of_field = timezone(timezone_to_test)
time_to_freeze = "2021-08-10 12:00"
data_fixture.create_text_field(table=table, name="text_field", primary=True)
last_modified_field_date = field_handler.create_field(
user=user,
table=table,
type_name="last_modified",
name="Last Date",
timezone=timezone_to_test,
)
last_modified_field_datetime = field_handler.create_field(
user=user,
table=table,
type_name="last_modified",
name="Last Datetime",
date_include_time=True,
timezone=timezone_to_test,
)
assert last_modified_field_date.date_include_time is False
assert last_modified_field_datetime.date_include_time is True
assert len(LastModifiedField.objects.all()) == 2
model = table.get_model(attribute_names=True)
# trying to create a row with values for the last_modified_field
# set will result in a ValidationError
with pytest.raises(ValidationError):
row_handler.create_row(
user=user, table=table, values={last_modified_field_date.id: "2021-08-09"}
)
with pytest.raises(ValidationError):
row_handler.create_row(
user=user,
table=table,
values={last_modified_field_datetime.id: "2021-08-09T14:14:33.574356Z"},
)
with freeze_time(time_to_freeze):
row = row_handler.create_row(user=user, table=table, values={}, model=model)
assert row.last_date is not None
assert row.last_date == row.updated_on
assert row.last_datetime is not None
row_last_modified_2 = row.last_datetime
row_updated_on = row.updated_on
assert row_last_modified_2 == row_updated_on
# Trying to update the the last_modified field will raise error
with pytest.raises(ValidationError):
row_handler.update_row(
user=user,
row_id=row.id,
table=table,
values={last_modified_field_date.id: "2021-08-09"},
)
with pytest.raises(ValidationError):
row_handler.update_row(
user=user,
table=table,
row_id=row.id,
values={last_modified_field_datetime.id: "2021-08-09T14:14:33.574356Z"},
)
# Updating the text field will updated
# the last_modified datetime field.
row_last_datetime_before_update = row.last_datetime
with freeze_time(time_to_freeze):
row_handler.update_row(
user=user,
table=table,
row_id=row.id,
values={
"text_field": "Hello Test",
},
model=model,
)
row.refresh_from_db()
assert row.last_datetime >= row_last_datetime_before_update
assert row.last_datetime == row.updated_on
row_last_modified_2_before_alter = row.last_datetime
# changing the field from LastModified to Datetime should persist the date
with freeze_time(time_to_freeze):
field_handler.update_field(
user=user,
field=last_modified_field_datetime,
new_type_name="date",
date_include_time=True,
)
assert len(LastModifiedField.objects.all()) == 1
row.refresh_from_db()
field_before_with_timezone = row_last_modified_2_before_alter.astimezone(
timezone_of_field
)
assert row.last_datetime.year == field_before_with_timezone.year
assert row.last_datetime.month == field_before_with_timezone.month
assert row.last_datetime.day == field_before_with_timezone.day
assert row.last_datetime.hour == field_before_with_timezone.hour
assert row.last_datetime.minute == field_before_with_timezone.minute
assert row.last_datetime.second == field_before_with_timezone.second
# changing the field from LastModified with Datetime to Text Field should persist
# the datetime as string
with freeze_time(time_to_freeze):
field_handler.update_field(
user=user,
field=last_modified_field_datetime,
new_type_name="last_modified",
date_include_time=True,
timezone="Europe/Berlin",
)
assert len(LastModifiedField.objects.all()) == 2
row.refresh_from_db()
row_last_modified_2_before_alter = row.last_datetime
field_handler.update_field(
user=user,
field=last_modified_field_datetime,
new_type_name="text",
)
row.refresh_from_db()
assert len(LastModifiedField.objects.all()) == 1
assert row.last_datetime == row_last_modified_2_before_alter.astimezone(
timezone_of_field
).strftime("%d/%m/%Y %H:%M")
# deleting the fields
field_handler.delete_field(user=user, field=last_modified_field_date)
assert len(LastModifiedField.objects.all()) == 0
@pytest.mark.django_db
def test_last_modified_field_type_wrong_timezone(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_handler = FieldHandler()
with pytest.raises(ValueError):
field_handler.create_field(
user=user,
table=table,
type_name="last_modified",
name="Last Date",
timezone="SDj",
)
@pytest.mark.django_db
def test_import_export_last_modified_field(data_fixture):
user = data_fixture.create_user()
imported_group = data_fixture.create_group(user=user)
database = data_fixture.create_database_application(user=user, name="Placeholder")
table = data_fixture.create_database_table(name="Example", database=database)
field_handler = FieldHandler()
last_modified_field = field_handler.create_field(
user=user,
table=table,
name="Last modified",
type_name="last_modified",
)
row_handler = RowHandler()
with freeze_time("2020-01-01 12:00"):
row = row_handler.create_row(
user=user,
table=table,
values={},
)
assert getattr(row, f"field_{last_modified_field.id}") == datetime(
2020, 1, 1, 12, 00, tzinfo=timezone("UTC")
)
core_handler = CoreHandler()
exported_applications = core_handler.export_group_applications(
database.group, BytesIO()
)
with freeze_time("2020-01-02 12:00"):
imported_applications, id_mapping = core_handler.import_applications_to_group(
imported_group, exported_applications, BytesIO(), None
)
imported_database = imported_applications[0]
imported_tables = imported_database.table_set.all()
imported_table = imported_tables[0]
imported_last_modified_field = imported_table.field_set.all().first().specific
imported_row = row_handler.get_row(user=user, table=imported_table, row_id=row.id)
assert imported_row.id == row.id
assert getattr(
imported_row, f"field_{imported_last_modified_field.id}"
) == datetime(2020, 1, 2, 12, 00, tzinfo=timezone("UTC"))

View file

@ -1206,6 +1206,446 @@ def test_date_equal_filter_type(data_fixture):
assert len(ids) == 4
@pytest.mark.django_db
def test_last_modified_date_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)
last_modified_field_date = data_fixture.create_last_modified_field(
table=table, date_include_time=False, timezone="Europe/Berlin"
)
last_modified_field_datetime = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/Berlin"
)
model = table.get_model()
with freeze_time("2021-08-04 21:59", tz_offset=+2):
row = model.objects.create(**{})
with freeze_time("2021-08-04 22:01", tz_offset=+2):
row_1 = model.objects.create(**{})
with freeze_time("2021-08-04 23:01", tz_offset=+2):
row_2 = model.objects.create(**{})
handler = ViewHandler()
model = table.get_model()
filter = data_fixture.create_view_filter(
view=grid_view,
field=last_modified_field_datetime,
type="date_equal",
value="2021-08-04",
)
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 = last_modified_field_date
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
assert row_1.id not in ids
assert row_2.id not in ids
@pytest.mark.django_db
def test_last_modified_day_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)
last_modified_field_datetime_berlin = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/Berlin"
)
last_modified_field_datetime_london = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/London"
)
handler = ViewHandler()
model = table.get_model()
def apply_filter():
return [
r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()
]
with freeze_time("2021-08-04 21:59"):
row = model.objects.create(**{})
with freeze_time("2021-08-04 22:01"):
row_1 = model.objects.create(**{})
with freeze_time("2021-08-04 23:01"):
row_2 = model.objects.create(**{})
filter = data_fixture.create_view_filter(
view=grid_view,
field=last_modified_field_datetime_london,
type="date_equals_today",
value="Europe/London",
)
with freeze_time("2021-08-04 01:00"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
with freeze_time("2021-08-04 22:59"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
with freeze_time("2021-08-04 23:59"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
with freeze_time("2021-08-04"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
with freeze_time("2021-08-05"):
# LastModified Column is based on London Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_london
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
@pytest.mark.django_db
def test_last_modified_month_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)
last_modified_field_datetime_berlin = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/Berlin"
)
last_modified_field_datetime_london = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/London"
)
handler = ViewHandler()
model = table.get_model()
def apply_filter():
return [
r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()
]
with freeze_time("2021-08-31 21:59"):
row = model.objects.create(**{})
with freeze_time("2021-08-31 22:01"):
row_1 = model.objects.create(**{})
with freeze_time("2021-08-31 23:01"):
row_2 = model.objects.create(**{})
filter = data_fixture.create_view_filter(
view=grid_view,
field=last_modified_field_datetime_london,
type="date_equals_month",
value="Europe/London",
)
with freeze_time("2021-08-31"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
with freeze_time("2021-09-01"):
# LastModified Column is based on London Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_london
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
@pytest.mark.django_db
def test_last_modified_year_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)
last_modified_field_datetime_berlin = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/Berlin"
)
last_modified_field_datetime_london = data_fixture.create_last_modified_field(
table=table, date_include_time=True, timezone="Europe/London"
)
handler = ViewHandler()
model = table.get_model()
def apply_filter():
return [
r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()
]
with freeze_time("2021-12-31 22:59"):
row = model.objects.create(**{})
with freeze_time("2021-12-31 23:01"):
row_1 = model.objects.create(**{})
with freeze_time("2022-01-01 00:01"):
row_2 = model.objects.create(**{})
filter = data_fixture.create_view_filter(
view=grid_view,
field=last_modified_field_datetime_london,
type="date_equals_year",
value="Europe/London",
)
with freeze_time("2021-12-31"):
# LastModified Column is based on London Time
# Filter value is based on London Time
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id in ids
assert row_1.id in ids
assert row_2.id not in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id in ids
assert row_1.id not in ids
assert row_2.id not in ids
with freeze_time("2022-01-01"):
# LastModified Column is based on London Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_london
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on London Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on London Time
filter.field = last_modified_field_datetime_berlin
filter.value = "Europe/London"
filter.save()
ids = apply_filter()
assert len(ids) == 1
assert row.id not in ids
assert row_1.id not in ids
assert row_2.id in ids
# LastModified Column is based on Berlin Time
# Filter value is based on Berlin Time
filter.value = "Europe/Berlin"
filter.save()
ids = apply_filter()
assert len(ids) == 2
assert row.id not in ids
assert row_1.id in ids
assert row_2.id in ids
@pytest.mark.django_db
def test_date_day_month_year_filter_type(data_fixture):
user = data_fixture.create_user()

View file

@ -13,6 +13,8 @@ from baserow.contrib.database.fields.models import (
URLField,
EmailField,
PhoneNumberField,
LastModifiedField,
CreatedOnField,
)
@ -227,3 +229,49 @@ class FieldFixtures:
self.create_model_field(kwargs["table"], field)
return field
def create_last_modified_field(self, user=None, create_field=True, **kwargs):
if "table" not in kwargs:
kwargs["table"] = self.create_database_table(user=user)
if "name" not in kwargs:
kwargs["name"] = self.fake.name()
if "order" not in kwargs:
kwargs["order"] = 0
if "date_include_time" not in kwargs:
kwargs["date_include_time"] = False
if "timezone" not in kwargs:
kwargs["timezone"] = "Europe/Berlin"
field = LastModifiedField.objects.create(**kwargs)
if create_field:
self.create_model_field(kwargs["table"], field)
return field
def create_created_on_field(self, user=None, create_field=True, **kwargs):
if "table" not in kwargs:
kwargs["table"] = self.create_database_table(user=user)
if "name" not in kwargs:
kwargs["name"] = self.fake.name()
if "order" not in kwargs:
kwargs["order"] = 0
if "date_include_time" not in kwargs:
kwargs["date_include_time"] = False
if "timezone" not in kwargs:
kwargs["timezone"] = "Europe/Berlin"
field = CreatedOnField.objects.create(**kwargs)
if create_field:
self.create_model_field(kwargs["table"], field)
return field

View file

@ -92,6 +92,14 @@ def setup_interesting_test_table(data_fixture):
"date_us": date,
"datetime_eu": datetime,
"date_eu": date,
"last_modified_datetime_us": None,
"last_modified_date_us": None,
"last_modified_datetime_eu": None,
"last_modified_date_eu": None,
"created_on_datetime_us": None,
"created_on_date_us": None,
"created_on_datetime_eu": None,
"created_on_date_eu": None,
# We will setup link rows manually later
"link_row": None,
"decimal_link_row": None,
@ -132,8 +140,13 @@ def setup_interesting_test_table(data_fixture):
if val is not None:
row_values[f"field_{name_to_field_id[field_type]}"] = val
# Make a blank row to test empty field conversion also.
blank_row = model.objects.create(**{})
row = model.objects.create(**row_values)
# We freeze time here so that we know what the values of the last_modified and
# created_on field types are going to be. Freezing the datetime will also freeze
# the current daylight savings time information.
with freeze_time("2021-01-02 12:00"):
blank_row = model.objects.create(**{})
row = model.objects.create(**row_values)
# Setup the link rows
linked_row_1 = row_handler.create_row(

View file

@ -23,6 +23,7 @@
* Enabled password validation in the backend.
* **Premium**: You can now comment and discuss rows with others in your group, click the
expand row button at the start of the row to view and add comments.
* Added "Last Modified" and "Created On" field types.
* New templates:
* Blog Post Management
* Updated templates:

View file

@ -44,6 +44,14 @@ def test_can_export_every_interesting_different_field_to_json(
"date_us": "",
"datetime_eu": "",
"date_eu": "",
"last_modified_datetime_us": "01/02/2021 13:00",
"last_modified_date_us": "01/02/2021",
"last_modified_datetime_eu": "02/01/2021 13:00",
"last_modified_date_eu": "02/01/2021",
"created_on_datetime_us": "01/02/2021 13:00",
"created_on_date_us": "01/02/2021",
"created_on_datetime_eu": "02/01/2021 13:00",
"created_on_date_eu": "02/01/2021",
"link_row": [],
"decimal_link_row": [],
"file_link_row": [],
@ -66,6 +74,14 @@ def test_can_export_every_interesting_different_field_to_json(
"date_us": "02/01/2020",
"datetime_eu": "01/02/2020 01:23",
"date_eu": "01/02/2020",
"last_modified_datetime_us": "01/02/2021 13:00",
"last_modified_date_us": "01/02/2021",
"last_modified_datetime_eu": "02/01/2021 13:00",
"last_modified_date_eu": "02/01/2021",
"created_on_datetime_us": "01/02/2021 13:00",
"created_on_date_us": "01/02/2021",
"created_on_datetime_eu": "02/01/2021 13:00",
"created_on_date_eu": "02/01/2021",
"link_row": [
"linked_row_1",
"linked_row_2",
@ -160,6 +176,14 @@ def test_can_export_every_interesting_different_field_to_xml(
<date-us/>
<datetime-eu/>
<date-eu/>
<last-modified-datetime-us>01/02/2021 13:00</last-modified-datetime-us>
<last-modified-date-us>01/02/2021</last-modified-date-us>
<last-modified-datetime-eu>02/01/2021 13:00</last-modified-datetime-eu>
<last-modified-date-eu>02/01/2021</last-modified-date-eu>
<created-on-datetime-us>01/02/2021 13:00</created-on-datetime-us>
<created-on-date-us>01/02/2021</created-on-date-us>
<created-on-datetime-eu>02/01/2021 13:00</created-on-datetime-eu>
<created-on-date-eu>02/01/2021</created-on-date-eu>
<link-row/>
<decimal-link-row/>
<file-link-row/>
@ -182,6 +206,14 @@ def test_can_export_every_interesting_different_field_to_xml(
<date-us>02/01/2020</date-us>
<datetime-eu>01/02/2020 01:23</datetime-eu>
<date-eu>01/02/2020</date-eu>
<last-modified-datetime-us>01/02/2021 13:00</last-modified-datetime-us>
<last-modified-date-us>01/02/2021</last-modified-date-us>
<last-modified-datetime-eu>02/01/2021 13:00</last-modified-datetime-eu>
<last-modified-date-eu>02/01/2021</last-modified-date-eu>
<created-on-datetime-us>01/02/2021 13:00</created-on-datetime-us>
<created-on-date-us>01/02/2021</created-on-date-us>
<created-on-datetime-eu>02/01/2021 13:00</created-on-datetime-eu>
<created-on-date-eu>02/01/2021</created-on-date-eu>
<link-row>
<item>linked_row_1</item>
<item>linked_row_2</item>

View file

@ -10,3 +10,7 @@
width: 20%;
margin-left: 4%;
}
.field-date-read-only-timestamp {
color: $color-neutral-400;
}

View file

@ -39,17 +39,28 @@ export default {
this.loading = true
const type = values.type
const fieldType = this.$registry.get('field', type)
delete values.type
try {
await this.$store.dispatch('field/create', {
const forceCreateCallback = await this.$store.dispatch('field/create', {
type,
values,
table: this.table,
forceCreate: false,
})
this.loading = false
this.$refs.form.reset()
this.hide()
const callback = async () => {
await forceCreateCallback()
this.createdId = null
this.loading = false
this.$refs.form.reset()
this.hide()
}
if (fieldType.shouldRefreshWhenAdded()) {
this.$emit('refresh', { callback })
} else {
await callback()
}
} catch (error) {
this.loading = false
notifyIf(error, 'field')

View file

@ -0,0 +1,39 @@
<template>
<div>
<FieldDateSubForm
:table="table"
:default-values="defaultValues"
></FieldDateSubForm>
<div class="control">
<div class="control__elements">
<div class="filters__value-timezone">{{ values.timezone }}</div>
</div>
</div>
</div>
</template>
<script>
import form from '@baserow/modules/core/mixins/form'
import FieldDateSubForm from '@baserow/modules/database/components/field/FieldDateSubForm'
import fieldSubForm from '@baserow/modules/database/mixins/fieldSubForm'
export default {
name: 'FieldCreatedOnLastModifiedSubForm',
components: { FieldDateSubForm },
mixins: [form, fieldSubForm],
data() {
return {
allowedValues: ['timezone'],
values: {
timezone: this.getCurrentTimezone(),
},
}
},
methods: {
getCurrentTimezone() {
return new Intl.DateTimeFormat().resolvedOptions().timeZone
},
},
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<div class="control__elements">
<div class="field-date-read-only-timestamp">
{{ getDate(field, value)
}}<template v-if="field.date_include_time"
>&nbsp;{{ getTime(field, value) }}</template
>
</div>
</div>
</template>
<script>
import rowEditField from '@baserow/modules/database/mixins/rowEditField'
import readOnlyDateField from '@baserow/modules/database/mixins/readOnlyDateField'
export default {
mixins: [rowEditField, readOnlyDateField],
}
</script>

View file

@ -34,6 +34,7 @@
<CreateFieldContext
ref="createFieldContext"
:table="table"
@refresh="$emit('refresh', $event)"
></CreateFieldContext>
</div>
</template>

View file

@ -125,6 +125,7 @@
:fields="fields"
:rows="allRows"
:read-only="readOnly"
@refresh="$emit('refresh', $event)"
@update="updateValue"
@hidden="rowEditModalHidden"
@field-updated="$emit('refresh', $event)"

View file

@ -33,6 +33,7 @@
<CreateFieldContext
ref="createFieldContext"
:table="table"
@refresh="$emit('refresh', $event)"
></CreateFieldContext>
</div>
</div>

View file

@ -19,32 +19,20 @@
</template>
<script>
import moment from 'moment'
import {
getDateMomentFormat,
getTimeMomentFormat,
} from '@baserow/modules/database/utils/date'
import readOnlyDateField from '@baserow/modules/database/mixins/readOnlyDateField'
export default {
name: 'FunctionalGridViewFieldDate',
methods: {
getDate(field, value) {
if (value === null) {
return ''
}
const existing = moment.utc(value || undefined)
const dateFormat = getDateMomentFormat(field.date_format)
return existing.format(dateFormat)
mixins: [readOnlyDateField],
props: {
field: {
type: Object,
required: true,
},
getTime(field, value) {
if (value === null) {
return ''
}
const existing = moment.utc(value || undefined)
const timeFormat = getTimeMomentFormat(field.date_time_format)
return existing.format(timeFormat)
value: {
type: String,
required: false,
default: '',
},
},
}

View file

@ -0,0 +1,29 @@
<template>
<div ref="cell" class="grid-view__cell active">
<div
class="grid-field-date"
:class="{ 'grid-field-date--has-time': field.date_include_time }"
>
<div ref="dateDisplay" class="grid-field-date__date">
{{ getDate(field, value) }}
</div>
<div
v-if="field.date_include_time"
ref="timeDisplay"
class="grid-field-date__time"
>
{{ getTime(field, value) }}
</div>
</div>
</div>
</template>
<script>
import gridField from '@baserow/modules/database/mixins/gridField'
import readOnlyDateField from '@baserow/modules/database/mixins/readOnlyDateField'
export default {
name: 'GridViewFieldDateReadOnly',
mixins: [gridField, readOnlyDateField],
}
</script>

View file

@ -11,6 +11,7 @@ import { Registerable } from '@baserow/modules/core/registry'
import FieldNumberSubForm from '@baserow/modules/database/components/field/FieldNumberSubForm'
import FieldTextSubForm from '@baserow/modules/database/components/field/FieldTextSubForm'
import FieldDateSubForm from '@baserow/modules/database/components/field/FieldDateSubForm'
import FieldCreatedOnLastModifiedSubForm from '@baserow/modules/database/components/field/FieldCreatedOnLastModifiedSubForm'
import FieldLinkRowSubForm from '@baserow/modules/database/components/field/FieldLinkRowSubForm'
import FieldSingleSelectSubForm from '@baserow/modules/database/components/field/FieldSingleSelectSubForm'
@ -22,6 +23,7 @@ import GridViewFieldLinkRow from '@baserow/modules/database/components/view/grid
import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/fields/GridViewFieldNumber'
import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/fields/GridViewFieldBoolean'
import GridViewFieldDate from '@baserow/modules/database/components/view/grid/fields/GridViewFieldDate'
import GridViewFieldDateReadOnly from '@baserow/modules/database/components/view/grid/fields/GridViewFieldDateReadOnly'
import GridViewFieldFile from '@baserow/modules/database/components/view/grid/fields/GridViewFieldFile'
import GridViewFieldSingleSelect from '@baserow/modules/database/components/view/grid/fields/GridViewFieldSingleSelect'
import GridViewFieldPhoneNumber from '@baserow/modules/database/components/view/grid/fields/GridViewFieldPhoneNumber'
@ -44,6 +46,7 @@ import RowEditFieldLinkRow from '@baserow/modules/database/components/row/RowEdi
import RowEditFieldNumber from '@baserow/modules/database/components/row/RowEditFieldNumber'
import RowEditFieldBoolean from '@baserow/modules/database/components/row/RowEditFieldBoolean'
import RowEditFieldDate from '@baserow/modules/database/components/row/RowEditFieldDate'
import RowEditFieldDateReadOnly from '@baserow/modules/database/components/row/RowEditFieldDateReadOnly'
import RowEditFieldFile from '@baserow/modules/database/components/row/RowEditFieldFile'
import RowEditFieldSingleSelect from '@baserow/modules/database/components/row/RowEditFieldSingleSelect'
import RowEditFieldPhoneNumber from '@baserow/modules/database/components/row/RowEditFieldPhoneNumber'
@ -184,6 +187,7 @@ export class FieldType extends Registerable {
this.sortIndicator = this.getSortIndicator()
this.canSortInView = this.getCanSortInView()
this.canBePrimaryField = this.getCanBePrimaryField()
this.isReadOnly = this.getIsReadOnly()
if (this.type === null) {
throw new Error('The type name of a view type must be set.')
@ -216,6 +220,7 @@ export class FieldType extends Registerable {
name: this.name,
sortIndicator: this.sortIndicator,
canSortInView: this.canSortInView,
isReadOnly: this.isReadOnly,
}
}
@ -375,6 +380,60 @@ export class FieldType extends Registerable {
)
)
}
/**
* 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
* is for example used by the last modified field type to update the last modified
* value in real time when a row has changed.
*/
onRowChange(
row,
updatedField,
updatedFieldValue,
updatedFieldOldValue,
currentField,
currentFieldValue
) {
return currentFieldValue
}
/**
* Is called for each field in the row when a row has moved to another position.
* Optionally, a different value can be returned here for that field. This is for
* example used by the last modified field type to update the last modified value
* in real time when a row has moved.
*/
onRowMove(row, order, oldOrder, currentField, currentFieldValue) {
return currentFieldValue
}
/**
* Is called for each field in a row when a new row is being created. This can be
* used to set a default value. This value will be added to the row before the
* call submitted to the backend, so the user will immediately see it.
*/
getNewRowValue(field) {
return this.getEmptyValue(field)
}
/**
* Determines whether a view refresh should be executed after the specific field
* has been added to a table. This is for example needed when a value depends on
* the backend and can't be guessed or calculated by the web-frontend.
*/
shouldRefreshWhenAdded() {
return false
}
/**
* Determines whether the fieldType is a read only field. Read only fields will be
* excluded from update requests to the backend. It is also not possible to change
* the value by for example pasting.
*/
getIsReadOnly() {
return false
}
}
export class TextFieldType extends FieldType {
@ -828,39 +887,19 @@ export class BooleanFieldType extends FieldType {
}
}
export class DateFieldType extends FieldType {
static getType() {
return 'date'
}
class BaseDateFieldType extends FieldType {
getIconClass() {
return 'calendar-alt'
}
getName() {
return 'Date'
getSortIndicator() {
return ['text', '1', '9']
}
getFormComponent() {
return FieldDateSubForm
}
getGridViewFieldComponent() {
return GridViewFieldDate
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldDate
}
getRowEditFieldComponent() {
return RowEditFieldDate
}
getSortIndicator() {
return ['text', '1', '9']
}
getSort(name, order) {
return (a, b) => {
if (a[name] === b[name]) {
@ -940,6 +979,168 @@ export class DateFieldType extends FieldType {
}
}
export class DateFieldType extends BaseDateFieldType {
static getType() {
return 'date'
}
getName() {
return 'Date'
}
getGridViewFieldComponent() {
return GridViewFieldDate
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldDate
}
getRowEditFieldComponent() {
return RowEditFieldDate
}
}
export class CreatedOnLastModifiedBaseFieldType extends BaseDateFieldType {
getIsReadOnly() {
return true
}
getFormComponent() {
return FieldCreatedOnLastModifiedSubForm
}
getFormViewFieldComponent() {
return null
}
getRowEditFieldComponent() {
return RowEditFieldDateReadOnly
}
getGridViewFieldComponent() {
return GridViewFieldDateReadOnly
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldDate
}
/**
* The "new row" value for the new row in the case of LastModified or CreatedOn Fields
* is simply the current time.
*/
getNewRowValue() {
return moment().utc().format()
}
shouldRefreshWhenAdded() {
return true
}
toHumanReadableString(field, value) {
const date = moment.tz(value, field.timezone)
if (date.isValid()) {
const dateFormat = getDateMomentFormat(field.date_format)
let dateString = date.format(dateFormat)
if (field.date_include_time) {
const timeFormat = getTimeMomentFormat(field.date_time_format)
dateString = `${dateString} ${date.format(timeFormat)}`
}
return dateString
} else {
return ''
}
}
prepareValueForCopy(field, value) {
return this.toHumanReadableString(field, value)
}
getDocsDataType(field) {
return null
}
getDocsDescription(field, firstPartOverwrite) {
const firstPart = firstPartOverwrite || 'This is a read only field.'
return field.date_include_time
? `${firstPart} The response will be a datetime in ISO format.`
: `${firstPart} The response will be a date in ISO format.`
}
getDocsRequestExample(field) {
return field.date_include_time ? '2020-01-01T12:00:00Z' : '2020-01-01'
}
getContainsFilterFunction() {
return genericContainsFilter
}
}
export class LastModifiedFieldType extends CreatedOnLastModifiedBaseFieldType {
static getType() {
return 'last_modified'
}
getIconClass() {
return 'edit'
}
getName() {
return 'Last Modified'
}
getDocsDescription(field) {
return super.getDocsDescription(
field,
'The last modified field is a read only field.'
)
}
_onRowChangeOrMove() {
return moment().utc().format()
}
onRowChange(
row,
updatedField,
updatedFieldValue,
updatedFieldOldValue,
currentField,
currentFieldValue
) {
return this._onRowChangeOrMove()
}
onRowMove(row, order, oldOrder, currentField, currentFieldValue) {
return this._onRowChangeOrMove()
}
}
export class CreatedOnFieldType extends CreatedOnLastModifiedBaseFieldType {
static getType() {
return 'created_on'
}
getIconClass() {
return 'plus'
}
getDocsDescription(field) {
return super.getDocsDescription(
field,
'The created on field is a read only field.'
)
}
getName() {
return 'Created On'
}
}
export class URLFieldType extends FieldType {
static getType() {
return 'url'

View file

@ -163,7 +163,11 @@ export default {
.get('field', this.field.type)
.prepareValueForPaste(this.field, event.clipboardData)
const oldValue = this.value
if (value !== oldValue && !this.readOnly) {
if (
value !== oldValue &&
!this.readOnly &&
!this.field._.type.isReadOnly
) {
this.$emit('update', value, oldValue)
}
}

View file

@ -0,0 +1,31 @@
import moment from 'moment'
import {
getDateMomentFormat,
getTimeMomentFormat,
} from '@baserow/modules/database/utils/date'
export default {
methods: {
getTimezone(field) {
return field.timezone || 'UTC'
},
getDate(field, value) {
if (value === null) {
return ''
}
const existing = moment.tz(value || undefined, this.getTimezone(field))
const dateFormat = getDateMomentFormat(field.date_format)
return existing.format(dateFormat)
},
getTime(field, value) {
if (value === null) {
return ''
}
const existing = moment.tz(value || undefined, this.getTimezone(field))
const timeFormat = getTimeMomentFormat(field.date_time_format)
return existing.format(timeFormat)
},
},
}

View file

@ -697,7 +697,7 @@
<h4 class="api-docs__heading-4">Request body schema</h4>
<ul class="api-docs__parameters">
<APIDocsParameter
v-for="field in fields[table.id]"
v-for="field in withoutReadOnly[table.id]"
:key="field.id"
:name="'field_' + field.id"
:visible-name="field.name"
@ -765,7 +765,7 @@
<h4 class="api-docs__heading-4">Request body schema</h4>
<ul class="api-docs__parameters">
<APIDocsParameter
v-for="field in fields[table.id]"
v-for="field in withoutReadOnly[table.id]"
:key="field.id"
:name="'field_' + field.id"
:visible-name="field.name"
@ -1007,6 +1007,7 @@ export default {
}
const fields = {}
const withoutReadOnly = {}
const populateField = (field) => {
const fieldType = app.$registry.get('field', field.type)
field._ = {
@ -1015,6 +1016,7 @@ export default {
requestExample: fieldType.getDocsRequestExample(field),
responseExample: fieldType.getDocsResponseExample(field),
fieldResponseExample: fieldType.getDocsFieldResponseExample(field),
isReadOnly: fieldType.isReadOnly,
}
return field
}
@ -1023,9 +1025,12 @@ export default {
const table = database.tables[i]
const { data } = await FieldService(app.$client).fetchAll(table.id)
fields[table.id] = data.map((field) => populateField(field))
withoutReadOnly[table.id] = fields[table.id].filter(
(field) => !field._.isReadOnly
)
}
return { database, fields }
return { database, fields, withoutReadOnly }
},
data() {
return {
@ -1110,7 +1115,19 @@ export default {
*/
getRequestExample(table, response = false) {
const item = {}
this.fields[table.id].forEach((field) => {
// In case we are creating a sample response
// read only fields need to be included.
// They should be left out in the case of
// creating a sample request.
let fieldsToLoopOver = this.fields[table.id]
if (!response) {
fieldsToLoopOver = fieldsToLoopOver.filter(
(field) => !field._.isReadOnly
)
}
fieldsToLoopOver.forEach((field) => {
const example = response
? field._.responseExample
: field._.requestExample

View file

@ -9,9 +9,11 @@ import {
NumberFieldType,
BooleanFieldType,
DateFieldType,
LastModifiedFieldType,
FileFieldType,
SingleSelectFieldType,
PhoneNumberFieldType,
CreatedOnFieldType,
} from '@baserow/modules/database/fieldTypes'
import {
EqualViewFilterType,
@ -98,6 +100,8 @@ export default ({ store, app }) => {
app.$registry.register('field', new NumberFieldType())
app.$registry.register('field', new BooleanFieldType())
app.$registry.register('field', new DateFieldType())
app.$registry.register('field', new LastModifiedFieldType())
app.$registry.register('field', new CreatedOnFieldType())
app.$registry.register('field', new URLFieldType())
app.$registry.register('field', new EmailFieldType())
app.$registry.register('field', new FileFieldType())

View file

@ -44,10 +44,25 @@ export const registerRealtimeEvents = (realtime) => {
}
})
realtime.registerEvent('field_created', ({ store }, data) => {
realtime.registerEvent('field_created', ({ store, app }, data) => {
const table = store.getters['table/getSelected']
const fieldType = app.$registry.get('field', data.field.type)
if (table !== undefined && table.id === data.field.table_id) {
store.dispatch('field/forceCreate', { table, values: data.field })
const callback = async () => {
await store.dispatch('field/forceCreate', {
table,
values: data.field,
})
}
if (!fieldType.shouldRefreshWhenAdded()) {
callback()
} else {
app.$bus.$emit('table-refresh', {
tableId: store.getters['table/getSelectedId'],
includeFieldOptions: true,
callback,
})
}
}
})

View file

@ -104,7 +104,7 @@ export const actions = {
/**
* Creates a new field with the provided type for the given table.
*/
async create(context, { type, table, values }) {
async create(context, { type, table, values, forceCreate = true }) {
const { dispatch } = context
if (Object.prototype.hasOwnProperty.call(values, 'type')) {
@ -122,7 +122,11 @@ export const actions = {
postData.type = type
const { data } = await FieldService(this.$client).create(table.id, postData)
dispatch('forceCreate', { table, values: data })
const forceCreateCallback = async () => {
return await dispatch('forceCreate', { table, values: data })
}
return forceCreate ? await forceCreateCallback() : forceCreateCallback
},
/**
* Restores a field into the field store and notifies the selected view that the

View file

@ -856,13 +856,21 @@ export const actions = {
) {
// Fill the not provided values with the empty value of the field type so we can
// immediately commit the created row to the state.
const valuesForApiRequest = {}
const allFields = [primary].concat(fields)
allFields.forEach((field) => {
const name = `field_${field.id}`
if (!(name in values)) {
const fieldType = this.$registry.get('field', field._.type.type)
const empty = fieldType.getEmptyValue(field)
const empty = fieldType.getNewRowValue(field)
values[name] = empty
// In case the fieldType is a read only field, we
// need to create a second values dictionary, which gets
// sent to the API without the fieldType.
if (!fieldType.isReadOnly) {
valuesForApiRequest[name] = empty
}
}
})
@ -896,7 +904,7 @@ export const actions = {
try {
const { data } = await RowService(this.$client).create(
table.id,
values,
valuesForApiRequest,
before !== null ? before.id : null
)
commit('FINALIZE_ROW_IN_BUFFER', {
@ -993,12 +1001,35 @@ export const actions = {
order = new BigNumber(before.order).minus(change).toString()
}
// In order to make changes feel really fast, we optimistically
// updated all the field values that provide a onRowMove function
const fieldsToCallOnRowMove = [...fields, primary]
const optimisticFieldValues = {}
const valuesBeforeOptimisticUpdate = {}
fieldsToCallOnRowMove.forEach((field) => {
const fieldType = this.$registry.get('field', field._.type.type)
const fieldID = `field_${field.id}`
const currentFieldValue = row[fieldID]
const fieldValue = fieldType.onRowMove(
row,
order,
oldOrder,
field,
currentFieldValue
)
if (currentFieldValue !== fieldValue) {
optimisticFieldValues[fieldID] = fieldValue
valuesBeforeOptimisticUpdate[fieldID] = currentFieldValue
}
})
dispatch('updatedExistingRow', {
view: grid,
fields,
primary,
row,
values: { order },
values: { order, ...optimisticFieldValues },
})
try {
@ -1007,6 +1038,9 @@ export const actions = {
row.id,
before !== null ? before.id : null
)
// Use the return value to update the moved row with values from
// the backend
commit('UPDATE_ROW_IN_BUFFER', { row, values: data })
if (before === null) {
// Not having a before means that the row was moved to the end and because
// that order was just an estimation, we want to update it with the real
@ -1024,7 +1058,7 @@ export const actions = {
fields,
primary,
row,
values: { order: oldOrder },
values: { order: oldOrder, ...valuesBeforeOptimisticUpdate },
})
throw error
}
@ -1038,7 +1072,47 @@ export const actions = {
{ commit, dispatch },
{ table, view, row, field, fields, primary, value, oldValue }
) {
// Immediately updated the store with the updated row field
// value.
commit('UPDATE_ROW_FIELD_VALUE', { row, field, value })
const optimisticFieldValues = {}
const valuesBeforeOptimisticUpdate = {}
// Store the before value of the field that gets updated
// in case we need to rollback changes
valuesBeforeOptimisticUpdate[field.id] = oldValue
let fieldsToCallOnRowChange = [...fields, primary]
// We already added the updated field values to the store
// so we can remove the field from our fieldsToCallOnRowChange
fieldsToCallOnRowChange = fieldsToCallOnRowChange.filter((el) => {
return el.id !== field.id
})
fieldsToCallOnRowChange.forEach((fieldToCall) => {
const fieldType = this.$registry.get('field', fieldToCall._.type.type)
const fieldID = `field_${fieldToCall.id}`
const currentFieldValue = row[fieldID]
const optimisticFieldValue = fieldType.onRowChange(
row,
field,
value,
oldValue,
fieldToCall,
currentFieldValue
)
if (currentFieldValue !== optimisticFieldValue) {
optimisticFieldValues[fieldID] = optimisticFieldValue
valuesBeforeOptimisticUpdate[fieldID] = currentFieldValue
}
})
commit('UPDATE_ROW_IN_BUFFER', {
row,
values: { ...optimisticFieldValues },
})
dispatch('onRowChange', { view, row, fields, primary })
const fieldType = this.$registry.get('field', field._.type.type)
@ -1047,9 +1121,18 @@ export const actions = {
values[`field_${field.id}`] = newValue
try {
await RowService(this.$client).update(table.id, row.id, values)
const updatedRow = await RowService(this.$client).update(
table.id,
row.id,
values
)
commit('UPDATE_ROW_IN_BUFFER', { row, values: updatedRow.data })
} catch (error) {
commit('UPDATE_ROW_FIELD_VALUE', { row, field, value: oldValue })
commit('UPDATE_ROW_IN_BUFFER', {
row,
values: { ...valuesBeforeOptimisticUpdate },
})
dispatch('onRowChange', { view, row, fields, primary })
throw error
}

View file

@ -197,6 +197,8 @@ export class ContainsViewFilterType extends ViewFilterType {
'email',
'phone_number',
'date',
'last_modified',
'created_on',
'single_select',
'number',
]
@ -228,6 +230,8 @@ export class ContainsNotViewFilterType extends ViewFilterType {
'email',
'phone_number',
'date',
'last_modified',
'created_on',
'single_select',
'number',
]
@ -256,7 +260,7 @@ export class DateEqualViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['date']
return ['date', 'last_modified', 'created_on']
}
matches(rowValue, filterValue, field, fieldType) {
@ -264,8 +268,12 @@ export class DateEqualViewFilterType extends ViewFilterType {
rowValue = ''
}
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, 10)
if (field.timezone) {
rowValue = moment.utc(rowValue).tz(field.timezone).format('YYYY-MM-DD')
} else {
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, 10)
}
return filterValue === '' || rowValue === filterValue
}
@ -289,14 +297,18 @@ export class DateBeforeViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['date']
return ['date', 'last_modified', 'created_on']
}
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')
let rowDate = moment.utc(rowValue)
const filterDate = moment.utc(filterValue)
if (field.timezone) {
rowDate = rowDate.tz(field.timezone)
}
// 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
@ -310,7 +322,7 @@ export class DateBeforeViewFilterType extends ViewFilterType {
return false
}
return rowDate.isBefore(filterDate)
return rowDate.isBefore(filterDate, 'day')
}
}
@ -332,14 +344,18 @@ export class DateAfterViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['date']
return ['date', 'last_modified', 'created_on']
}
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')
let rowDate = moment.utc(rowValue)
const filterDate = moment.utc(filterValue)
if (field.timezone) {
rowDate = rowDate.tz(field.timezone)
}
// 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
@ -353,7 +369,7 @@ export class DateAfterViewFilterType extends ViewFilterType {
return false
}
return rowDate.isAfter(filterDate)
return rowDate.isAfter(filterDate, 'day')
}
}
@ -375,7 +391,7 @@ export class DateNotEqualViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['date']
return ['date', 'last_modified', 'created_on']
}
matches(rowValue, filterValue, field, fieldType) {
@ -383,8 +399,12 @@ export class DateNotEqualViewFilterType extends ViewFilterType {
rowValue = ''
}
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, 10)
if (field.timezone) {
rowValue = moment.utc(rowValue).tz(field.timezone).format('YYYY-MM-DD')
} else {
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, 10)
}
return filterValue === '' || rowValue !== filterValue
}
@ -404,7 +424,7 @@ export class DateEqualsTodayViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['date']
return ['date', 'last_modified', 'created_on']
}
getDefaultValue() {
@ -420,17 +440,22 @@ export class DateEqualsTodayViewFilterType extends ViewFilterType {
return 10
}
matches(rowValue, filterValue) {
matches(rowValue, filterValue, field) {
if (rowValue === null) {
rowValue = ''
}
const sliceLength = this.getSliceLength()
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, sliceLength)
const format = 'YYYY-MM-DD'.slice(0, sliceLength)
const today = moment().tz(filterValue).format(format)
if (field.timezone) {
rowValue = moment.utc(rowValue).tz(field.timezone).format(format)
} else {
rowValue = rowValue.toString().toLowerCase().trim()
rowValue = rowValue.slice(0, sliceLength)
}
return rowValue === today
}
}
@ -703,6 +728,8 @@ export class EmptyViewFilterType extends ViewFilterType {
'email',
'number',
'date',
'last_modified',
'created_on',
'boolean',
'link_row',
'file',
@ -746,6 +773,8 @@ export class NotEmptyViewFilterType extends ViewFilterType {
'email',
'number',
'date',
'last_modified',
'created_on',
'boolean',
'link_row',
'file',

View file

@ -0,0 +1,403 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import moment from 'moment'
import {
DateBeforeViewFilterType,
DateAfterViewFilterType,
DateEqualViewFilterType,
DateNotEqualViewFilterType,
DateEqualsTodayViewFilterType,
} from '@baserow/modules/database/viewFilters'
const dateBeforeCasesWithTimezone = [
{
rowValue: '2021-08-10T21:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-10',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-10T22:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: true,
},
{
rowValue: '2021-08-10T22:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-10T23:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: false,
},
]
const dateBeforeCasesWithoutTimezone = [
{
rowValue: '2021-08-10T23:59:37.940086Z',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-10',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-11T00:01:37.940086Z',
filterValue: '2021-08-11',
expected: false,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
expected: false,
},
]
const dateAfterCasesWithTimezone = [
{
rowValue: '2021-08-11T22:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-12',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-10',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-11T23:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: true,
},
{
rowValue: '2021-08-11T21:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-11T22:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: false,
},
]
const dateAfterCasesWithoutTimezone = [
{
rowValue: '2021-08-12T00:01:37.940086Z',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-12',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-11T23:59:37.940086Z',
filterValue: '2021-08-11',
expected: false,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
expected: false,
},
]
const dateEqualCasesWithTimezone = [
{
rowValue: '2021-08-11T21:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-11T22:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: true,
},
{
rowValue: '2021-08-10T22:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-10T23:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: true,
},
{
rowValue: '2021-08-10T21:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-10T22:59:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: false,
},
]
const dateEqualWithoutTimezone = [
{
rowValue: '2021-08-11T23:59:37.940086Z',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-11T00:01:37.940086Z',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-12T00:01:37.940086Z',
filterValue: '2021-08-11',
expected: false,
},
{
rowValue: '2021-08-12',
filterValue: '2021-08-11',
expected: false,
},
]
const dateNotEqualCasesWithTimezone = [
{
rowValue: '2021-08-11T22:30:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-12',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: true,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-11T23:30:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: true,
},
{
rowValue: '2021-08-10T22:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/Berlin',
expected: false,
},
{
rowValue: '2021-08-10T23:01:37.940086Z',
filterValue: '2021-08-11',
timezone: 'Europe/London',
expected: false,
},
]
const dateNotEqualCasesWithoutTimezone = [
{
rowValue: '2021-08-11T23:59:37.940086Z',
filterValue: '2021-08-12',
expected: true,
},
{
rowValue: '2021-08-13T00:01:37.940086Z',
filterValue: '2021-08-12',
expected: true,
},
{
rowValue: '2021-08-10',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-12',
filterValue: '2021-08-11',
expected: true,
},
{
rowValue: '2021-08-11T22:59:37.940086Z',
filterValue: '2021-08-11',
expected: false,
},
{
rowValue: '2021-08-11',
filterValue: '2021-08-11',
expected: false,
},
]
const dateToday = [
{
rowValue: moment().utc().format(),
filterValue: 'Europe/Berlin',
expected: true,
},
{
rowValue: '1970-08-11T23:30:37.940086Z',
filterValue: 'Europe/Berlin',
expected: false,
},
]
describe('All Tests', () => {
let testApp = null
beforeAll(() => {
testApp = new TestApp()
})
afterEach(() => {
testApp.afterEach()
})
test.each(dateBeforeCasesWithTimezone)(
'BeforeViewFilter with Timezone',
(values) => {
const result = new DateBeforeViewFilterType().matches(
values.rowValue,
values.filterValue,
{ timezone: values.timezone }
)
expect(result).toBe(values.expected)
}
)
test.each(dateBeforeCasesWithoutTimezone)(
'BeforeViewFilter without Timezone',
(values) => {
const result = new DateBeforeViewFilterType().matches(
values.rowValue,
values.filterValue,
{}
)
expect(result).toBe(values.expected)
}
)
test.each(dateAfterCasesWithTimezone)(
'AfterViewFilter with Timezone',
(values) => {
const result = new DateAfterViewFilterType().matches(
values.rowValue,
values.filterValue,
{ timezone: values.timezone }
)
expect(result).toBe(values.expected)
}
)
test.each(dateAfterCasesWithoutTimezone)(
'AfterViewFilter without Timezone',
(values) => {
const result = new DateAfterViewFilterType().matches(
values.rowValue,
values.filterValue,
{}
)
expect(result).toBe(values.expected)
}
)
test.each(dateEqualCasesWithTimezone)('DateEqual with Timezone', (values) => {
const result = new DateEqualViewFilterType().matches(
values.rowValue,
values.filterValue,
{ timezone: values.timezone }
)
expect(result).toBe(values.expected)
})
test.each(dateEqualWithoutTimezone)(
'DateEqual without Timezone',
(values) => {
const result = new DateEqualViewFilterType().matches(
values.rowValue,
values.filterValue,
{ timezone: values.timezone }
)
expect(result).toBe(values.expected)
}
)
test.each(dateNotEqualCasesWithTimezone)(
'DateNotEqual with Timezone',
(values) => {
const result = new DateNotEqualViewFilterType().matches(
values.rowValue,
values.filterValue,
{ timezone: values.timezone }
)
expect(result).toBe(values.expected)
}
)
test.each(dateNotEqualCasesWithoutTimezone)(
'DateNotEqual without Timezone',
(values) => {
const result = new DateNotEqualViewFilterType().matches(
values.rowValue,
values.filterValue,
{}
)
expect(result).toBe(values.expected)
}
)
test.each(dateToday)('DateToday', (values) => {
const result = new DateEqualsTodayViewFilterType().matches(
values.rowValue,
values.filterValue,
{}
)
expect(result).toBe(values.expected)
})
})