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:
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
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}),
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue