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:
parent
f574d7c145
commit
4696b98ac4
40 changed files with 2760 additions and 188 deletions
backend
src/baserow
tests
premium/backend/tests/baserow_premium/export
web-frontend
modules
core/assets/scss/components/fields
database
test/unit/database
|
@ -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())
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
112
backend/src/baserow/contrib/database/fields/mixins.py
Normal file
112
backend/src/baserow/contrib/database/fields/mixins.py
Normal 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)
|
|
@ -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):
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
46
backend/src/baserow/core/expressions.py
Normal file
46
backend/src/baserow/core/expressions.py
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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",
|
||||
{
|
||||
|
|
|
@ -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",'
|
||||
|
|
|
@ -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")
|
||||
)
|
|
@ -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",
|
||||
|
|
|
@ -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"))
|
|
@ -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()
|
||||
|
|
48
backend/tests/fixtures/field.py
vendored
48
backend/tests/fixtures/field.py
vendored
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -10,3 +10,7 @@
|
|||
width: 20%;
|
||||
margin-left: 4%;
|
||||
}
|
||||
|
||||
.field-date-read-only-timestamp {
|
||||
color: $color-neutral-400;
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
> {{ 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>
|
|
@ -34,6 +34,7 @@
|
|||
<CreateFieldContext
|
||||
ref="createFieldContext"
|
||||
:table="table"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
:fields="fields"
|
||||
:rows="allRows"
|
||||
:read-only="readOnly"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@update="updateValue"
|
||||
@hidden="rowEditModalHidden"
|
||||
@field-updated="$emit('refresh', $event)"
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<CreateFieldContext
|
||||
ref="createFieldContext"
|
||||
:table="table"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
31
web-frontend/modules/database/mixins/readOnlyDateField.js
Normal file
31
web-frontend/modules/database/mixins/readOnlyDateField.js
Normal 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)
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
403
web-frontend/test/unit/database/viewFiltersMatch.spec.js
Normal file
403
web-frontend/test/unit/database/viewFiltersMatch.spec.js
Normal 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)
|
||||
})
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue