1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-03-16 05:23:33 +00:00

Resolve "Support formula date fields in the calendar view"

This commit is contained in:
Nigel Gott 2023-05-15 11:18:29 +00:00
parent fdfb8187ee
commit c74eee3d7d
16 changed files with 168 additions and 118 deletions
backend/src/baserow/contrib/database
changelog/entries/unreleased/feature
premium
backend
src/baserow_premium/views
tests/baserow_premium_tests
web-frontend/modules/baserow_premium
web-frontend/modules/database

View file

@ -698,7 +698,9 @@ class DateFieldType(FieldType):
api_exceptions_map = {
DateForceTimezoneOffsetValueError: ERROR_DATE_FORCE_TIMEZONE_OFFSET_ERROR
}
can_represent_date = True
def can_represent_date(self, field):
return True
def get_request_kwargs_to_backup(self, field, kwargs) -> Dict[str, Any]:
date_force_timezone_offset = kwargs.get("date_force_timezone_offset", None)
@ -3518,6 +3520,9 @@ class FormulaFieldType(ReadOnlyFieldType):
) -> bool:
return False
def can_represent_date(self, field: "Field") -> bool:
return self.to_baserow_formula_type(field.specific).can_represent_date
class LookupFieldType(FormulaFieldType):
type = "lookup"

View file

@ -96,9 +96,6 @@ class FieldType(
`FieldHandler::get_unique_row_values` method.
"""
can_represent_date = False
"""Indicates whether the field can be used to represent date or datetime."""
read_only = False
"""Indicates whether the field allows inserting/updating row values or if it is
read only."""
@ -1450,6 +1447,11 @@ class FieldType(
return getattr(row, field_name)
def can_represent_date(self, field):
"""Indicates whether the field can be used to represent date or datetime."""
return False
class ReadOnlyFieldHasNoInternalDbValueError(Exception):
"""

View file

@ -168,6 +168,10 @@ class BaserowFormulaType(abc.ABC):
pass
@property
def can_represent_date(self) -> bool:
return False
@property
@abc.abstractmethod
def is_valid(self) -> bool:

View file

@ -431,6 +431,7 @@ class BaserowFormulaDateType(BaserowFormulaValidType):
"date_force_timezone",
]
nullable_option_fields = ["date_force_timezone"]
can_represent_date = True
def __init__(
self,
@ -739,7 +740,6 @@ class BaserowFormulaSingleSelectType(BaserowFormulaValidType):
return []
def get_baserow_field_instance_and_type(self):
# Until Baserow has a array field type implement the required methods below
return self, self
def get_model_field(self, instance, **kwargs) -> models.Field:

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "support formula date fields in the calendar view",
"issue_number": 1732,
"bullet_points": [],
"created_at": "2023-05-11"
}

View file

@ -178,7 +178,7 @@ def get_rows_grouped_by_date_field(
date_field = date_field.specific
date_field_type = field_type_registry.get_by_model(date_field)
if not date_field_type.can_represent_date:
if not date_field_type.can_represent_date(date_field):
raise CalendarViewHasNoDateField()
if base_queryset is None:

View file

@ -311,8 +311,9 @@ class CalendarViewType(ViewType):
pk=date_field_value
)
field_type = field_type_registry.get_by_model(date_field_value.specific)
if not field_type.can_represent_date:
date_field_value = date_field_value.specific
field_type = field_type_registry.get_by_model(date_field_value)
if not field_type.can_represent_date(date_field_value):
raise IncompatibleField()
if (

View file

@ -24,7 +24,7 @@ from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import View
from baserow.test_utils.helpers import is_dict_subset
from baserow.test_utils.helpers import is_dict_subset, setup_interesting_test_table
def get_list_url(calendar_view_id: int) -> str:
@ -574,19 +574,23 @@ def test_create_calendar_view_different_field_types(api_client, premium_data_fix
user, token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
table = premium_data_fixture.create_database_table(user=user)
table, _, _, _, context = setup_interesting_test_table(
premium_data_fixture, user=user
)
kwargs = {
"table": table,
"name": premium_data_fixture.fake.name(),
"order": 1,
}
all_field_types = field_type_registry.get_all()
date_field_types = [t for t in all_field_types if t.can_represent_date]
for date_field_type in date_field_types:
field = date_field_type.model_class.objects.create(**kwargs)
premium_data_fixture.create_model_field(kwargs["table"], field)
specific_fields = [f.specific for f in table.field_set.all()]
can_represent_date_fields = [
f
for f in specific_fields
if field_type_registry.get_by_model(f).can_represent_date(f)
]
for field in can_represent_date_fields:
response = api_client.post(
reverse("api:database:views:list", kwargs={"table_id": table.id}),
{

View file

@ -25,7 +25,10 @@ from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.registries import view_type_registry
from baserow.core.action.handler import ActionHandler
from baserow.core.action.registries import action_type_registry
from baserow.test_utils.helpers import assert_undo_redo_actions_are_valid
from baserow.test_utils.helpers import (
assert_undo_redo_actions_are_valid,
setup_interesting_test_table,
)
@pytest.mark.django_db
@ -178,18 +181,27 @@ def test_calendar_view_created(premium_data_fixture):
@pytest.mark.django_db
@pytest.mark.view_calendar
def test_calendar_view_convert_date_field_to_another(premium_data_fixture):
user = premium_data_fixture.create_user()
table = premium_data_fixture.create_database_table(user=user)
user, token = premium_data_fixture.create_user_and_token(
has_active_premium_license=True
)
table, _, _, _, context = setup_interesting_test_table(
premium_data_fixture, user=user
)
date_field = premium_data_fixture.create_date_field(table=table)
calendar_view = premium_data_fixture.create_calendar_view(
table=table, date_field=date_field
)
all_field_types = field_type_registry.get_all()
date_field_types = [t for t in all_field_types if t.can_represent_date]
specific_fields = [f.specific for f in table.field_set.all()]
can_represent_date_fields = [
f
for f in specific_fields
if field_type_registry.get_by_model(f).can_represent_date(f)
]
not_compatible_field_type = field_type_registry.get("text")
for date_field_type in date_field_types:
for date_field in can_represent_date_fields:
date_field_type = field_type_registry.get_by_model(date_field)
FieldHandler().update_field(
user=user, field=date_field, new_type_name=date_field_type.type
)
@ -491,95 +503,96 @@ def row_number(idx):
GET_ROWS_GROUPED_BY_DATE_FIELD_CASES = [
# (
# "basic date field test case",
# case(
# field=("date", "date"),
# rows=["2023-01-10", "2023-01-12"],
# from_timestamp=datetime(2023, 1, 9, 0, 0, 0, 0),
# to_timestamp=datetime(2023, 1, 11, 0, 0, 0, 0),
# user_timezone="UTC",
# expected_result={
# "2023-01-09": {"count": 0, "results": []},
# "2023-01-10": {"count": 1, "results": [row_number(0)]},
# },
# ),
# ),
# (
# "basic datetime field test case",
# case(
# field=("datetime", {"type": "date", "date_include_time": True}),
# rows=["2023-01-10 00:00", "2023-01-09 23:59"],
# from_timestamp=datetime(2023, 1, 10, 0, 0, 0, 0),
# to_timestamp=datetime(2023, 1, 11, 0, 0, 0, 0),
# user_timezone="UTC",
# expected_result={
# "2023-01-10": {"count": 1, "results": [row_number(0)]},
# },
# ),
# ),
# (
# "basic datetime field test case where user timezone changes result",
# case(
# field=("datetime", {"type": "date", "date_include_time": True}),
# rows=["2023-01-09 23:59", "2023-01-10 00:00"],
# from_timestamp=datetime(2023, 1, 9, 23, 0, 0, 0),
# to_timestamp=datetime(2023, 1, 10, 1, 0, 0, 0),
# user_timezone="CET",
# expected_result={
# "2023-01-10": {"count": 2, "results": [row_number(0), row_number(1)]},
# },
# ),
# ),
# (
# "datetime field test case where user timezone overriden by field timezone",
# case(
# field=(
# "datetime",
# {
# "type": "date",
# "date_include_time": True,
# "date_force_timezone": "Etc/GMT-2",
# },
# ),
# rows=["2023-01-09 23:59", "2023-01-10 00:00"],
# from_timestamp=datetime(2023, 1, 9, 23, 0, 0, 0),
# to_timestamp=datetime(2023, 1, 10, 1, 0, 0, 0),
# user_timezone="CET",
# expected_result={
# "2023-01-10": {"count": 2, "results": [row_number(0), row_number(1)]},
# },
# ),
# ),
# (
# "datetime field test case when CET clocks go forwards",
# case(
# field=(
# "datetime",
# {
# "type": "date",
# "date_include_time": True,
# },
# ),
# rows=[
# "2022-03-26 22:59", # 26th Europe/Amsterdam (UTC+1)
# "2022-03-26 23:00", # 27th Europe/Amsterdam (UTC+1)
# "2022-03-27 21:59", # 27th Europe/Amsterdam (UTC+2)
# "2022-03-27 22:00", # 28th Europe/Amsterdam (UTC+2)
# ],
# from_timestamp=datetime(2022, 3, 26, 0, 0, 0, 0),
# to_timestamp=datetime(2022, 3, 28, 1, 0, 0, 0),
# user_timezone="Europe/Amsterdam",
# expected_result={
# "2022-03-26": {"count": 1, "results": [row_number(0)]},
# "2022-03-27": {
# "count": 2,
# "results": [row_number(1), row_number(2)],
# },
# "2022-03-28": {"count": 1, "results": [row_number(3)]},
# },
# ),
# ),
(
"basic date field test case",
case(
field=("date", "date"),
rows=["2023-01-10", "2023-01-12"],
from_timestamp=datetime(2023, 1, 9, 0, 0, 0, 0),
to_timestamp=datetime(2023, 1, 11, 0, 0, 0, 0),
user_timezone="UTC",
expected_result={
"2023-01-09": {"count": 0, "results": []},
"2023-01-10": {"count": 1, "results": [row_number(0)]},
"2023-01-11": {"count": 0, "results": []},
},
),
),
(
"basic datetime field test case",
case(
field=("datetime", {"type": "date", "date_include_time": True}),
rows=["2023-01-10 00:00", "2023-01-09 23:59"],
from_timestamp=datetime(2023, 1, 10, 0, 0, 0, 0),
to_timestamp=datetime(2023, 1, 11, 0, 0, 0, 0),
user_timezone="UTC",
expected_result={
"2023-01-10": {"count": 1, "results": [row_number(0)]},
},
),
),
(
"basic datetime field test case where user timezone changes result",
case(
field=("datetime", {"type": "date", "date_include_time": True}),
rows=["2023-01-09 23:59", "2023-01-10 00:00"],
from_timestamp=datetime(2023, 1, 9, 23, 0, 0, 0),
to_timestamp=datetime(2023, 1, 10, 1, 0, 0, 0),
user_timezone="CET",
expected_result={
"2023-01-10": {"count": 2, "results": [row_number(0), row_number(1)]},
},
),
),
(
"datetime field test case where user timezone overriden by field timezone",
case(
field=(
"datetime",
{
"type": "date",
"date_include_time": True,
"date_force_timezone": "Etc/GMT-2",
},
),
rows=["2023-01-09 23:59", "2023-01-10 00:00"],
from_timestamp=datetime(2023, 1, 9, 23, 0, 0, 0),
to_timestamp=datetime(2023, 1, 10, 1, 0, 0, 0),
user_timezone="CET",
expected_result={
"2023-01-10": {"count": 2, "results": [row_number(0), row_number(1)]},
},
),
),
(
"datetime field test case when CET clocks go forwards",
case(
field=(
"datetime",
{
"type": "date",
"date_include_time": True,
},
),
rows=[
"2022-03-26 22:59", # 26th Europe/Amsterdam (UTC+1)
"2022-03-26 23:00", # 27th Europe/Amsterdam (UTC+1)
"2022-03-27 21:59", # 27th Europe/Amsterdam (UTC+2)
"2022-03-27 22:00", # 28th Europe/Amsterdam (UTC+2)
],
from_timestamp=datetime(2022, 3, 26, 0, 0, 0, 0),
to_timestamp=datetime(2022, 3, 28, 1, 0, 0, 0),
user_timezone="Europe/Amsterdam",
expected_result={
"2022-03-26": {"count": 1, "results": [row_number(0)]},
"2022-03-27": {
"count": 2,
"results": [row_number(1), row_number(2)],
},
"2022-03-28": {"count": 1, "results": [row_number(3)]},
},
),
),
(
"date field test case ignores user supplied timezone",
case(

View file

@ -224,7 +224,8 @@ export default {
openCreateRowModal(event) {
const defaults = {}
const dateField = this.getDateField(this.fields)
if (event?.day?.date != null && dateField) {
const fieldType = this.$registry.get('field', dateField.type)
if (event?.day?.date != null && dateField && !fieldType.getIsReadOnly()) {
const name = `field_${dateField.id}`
if (dateField.date_include_time) {
defaults[name] = event.day.date.toISOString()

View file

@ -106,7 +106,7 @@ export default {
const df = this.getDateField(this.fields)
if (
!df ||
this.$registry.get('field', df.type).canRepresentDate() === false
this.$registry.get('field', df.type).canRepresentDate(df) === false
) {
return this.$t('calendarViewHeader.displayBy')
} else {
@ -145,7 +145,7 @@ export default {
const df = this.getDateField(this.fields)
if (
!df ||
this.$registry.get('field', df.type).canRepresentDate() === false
this.$registry.get('field', df.type).canRepresentDate(df) === false
) {
this.showChooseDateFieldModal()
}

View file

@ -68,7 +68,7 @@ export default {
computed: {
dateFields() {
return this.fields.filter((f) => {
return this.$registry.get('field', f.type).canRepresentDate()
return this.$registry.get('field', f.type).canRepresentDate(f)
})
},
},

View file

@ -239,7 +239,7 @@ export const actions = {
const df = getters.getDateField(fields)
if (
!df ||
this.$registry.get('field', df.type).canRepresentDate() === false
this.$registry.get('field', df.type).canRepresentDate(df) === false
) {
commit('RESET')
return

View file

@ -388,7 +388,7 @@ export class CalendarViewType extends PremiumViewType {
if (dateFieldId === field.id) {
const type = this.app.$registry.get('field', field.type)
if (!type.canRepresentDate()) {
if (!type.canRepresentDate(field)) {
this._setFieldToNull(context, field, 'date_field')
await context.dispatch(
storePrefix + 'view/calendar/reset',

View file

@ -249,7 +249,7 @@ export class FieldType extends Registerable {
* When true, indicates a field type that can be used to
* represent a date.
*/
canRepresentDate() {
canRepresentDate(field) {
return false
}
@ -1524,7 +1524,7 @@ class BaseDateFieldType extends FieldType {
return true
}
canRepresentDate() {
canRepresentDate(field) {
return true
}
}
@ -2681,6 +2681,11 @@ export class FormulaFieldType extends FieldType {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getCanSortInView()
}
canRepresentDate(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.canRepresentDate(field)
}
}
export class LookupFieldType extends FormulaFieldType {

View file

@ -104,6 +104,10 @@ export class BaserowFormulaTypeDefinition extends Registerable {
)
return underlyingFieldType.toHumanReadableString(field, value)
}
canRepresentDate() {
return false
}
}
export class BaserowFormulaTextType extends BaserowFormulaTypeDefinition {
@ -234,6 +238,10 @@ export class BaserowFormulaDateType extends BaserowFormulaTypeDefinition {
getFunctionalGridViewFieldArrayComponent() {
return FunctionalFormulaDateArrayItem
}
canRepresentDate() {
return true
}
}
export class BaserowFormulaDateIntervalType extends BaserowFormulaTypeDefinition {