Looking up a date interval formula breaks the view
Describe the problem
- Create a formula field with the formula
date_interval('1 day')
- Link to this table from another table
- From the other table create a lookup field which looks up the formula field
- Observe you can no longer access the table:
AttributeError: 'str' object has no attribute 'days'
File "django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "baserow/api/decorators.py", line 83, in func_wrapper
return func(*args, **kwargs)
File "baserow/api/decorators.py", line 299, in func_wrapper
return func(*args, **kwargs)
File "baserow/contrib/database/api/views/grid/views.py", line 267, in get
response = paginator.get_paginated_response(serializer.data)
File "rest_framework/serializers.py", line 768, in data
ret = super().data
File "rest_framework/serializers.py", line 253, in data
self._data = self.to_representation(self.instance)
File "rest_framework/serializers.py", line 687, in to_representation
self.child.to_representation(item) for item in iterable
File "rest_framework/serializers.py", line 687, in <listcomp>
self.child.to_representation(item) for item in iterable
File "rest_framework/serializers.py", line 522, in to_representation
ret[field.field_name] = field.to_representation(attribute)
File "rest_framework/serializers.py", line 687, in to_representation
self.child.to_representation(item) for item in iterable
File "rest_framework/serializers.py", line 687, in <listcomp>
self.child.to_representation(item) for item in iterable
File "baserow/contrib/database/api/fields/serializers.py", line 128, in to_representation
return super().to_representation(instance)
File "rest_framework/serializers.py", line 522, in to_representation
ret[field.field_name] = field.to_representation(attribute)
File "rest_framework/fields.py", line 1403, in to_representation
return duration_string(value)
File "django/utils/duration.py", line 20, in duration_string
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
File "django/utils/duration.py", line 5, in _get_duration_components
days = duration.days
Test to reproduce
Index: backend/tests/baserow/contrib/database/api/fields/test_formula_views.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py b/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py
--- a/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py (revision ee3d1161b35791d29b65cc6bfdca06b25f96ac9b)
+++ b/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py (date 1651046706569)
@@ -7,6 +7,8 @@
HTTP_404_NOT_FOUND,
)
+from baserow.contrib.database.fields.handler import FieldHandler
+
@pytest.mark.django_db
def test_altering_value_of_referenced_field(data_fixture, api_client):
@@ -1423,3 +1425,42 @@
)
assert response.status_code == HTTP_200_OK, response_json
response_json = response.json()
+
+
+@pytest.mark.django_db
+def test_can_lookup_duration_formula(api_client, data_fixture):
+ user, token = data_fixture.create_user_and_token()
+ table_a, table_b, link_field = data_fixture.create_two_linked_tables(user)
+
+ FieldHandler().create_field(
+ user, table_a, "formula", formula="date_interval('1 day')", name="duration"
+ )
+
+ model_b = table_b.get_model()
+ row_b = model_b.objects.create()
+
+ model = table_a.get_model()
+ row_1 = model.objects.create()
+ getattr(row_1, f"field_{link_field.id}").set([row_b.id])
+ row_1.save()
+
+ lookup_field = FieldHandler().create_field(
+ user,
+ table_b,
+ "formula",
+ formula=f"lookup('{link_field.link_row_related_field.name}', 'duration')",
+ name="lookup",
+ )
+
+ response = api_client.get(
+ reverse("api:database:rows:list", kwargs={"table_id": table_b.id}),
+ {},
+ format="json",
+ HTTP_AUTHORIZATION=f"JWT {token}",
+ )
+ response_json = response.json()
+ assert response.status_code == HTTP_200_OK, response_json
+ assert response_json["count"] == 1
+ assert response_json["results"][0][lookup_field.db_column] == [
+ "1 day"
+ ], response_json
Cause
When we create a response serializer for a lookup field we call baserow.contrib.database.formula.types.formula_types.BaserowFormulaArrayType.get_serializer_field
which in turn will call the looked up values type to construct a ArrayValueSerializer
. In this situation we are looking up a BaserowFormulaDateIntervalType
and so we make an ArrayValueSerializer
with a child serializer of type serializers.DurationField
. However because in the database a lookup formula/field is stored as JSON, we are actually passing to this serializer an array of strings and not an array of timedelta python objects. However the serializer expects an array of timedelta python objects.
I think the nicest fix here is introducing a new method on formula types which would be get_array_serializer
which should return a serializer which knows how to parse the JSON encoded representation of that type. Then for the BaserowFormulaDateIntervalType we could just use a CharSerializer as it's already in a serialized format when converted to JSON.