Skip to content

Commit bedc1b4

Browse files
authored
Add support for Collaborator field to be upserted (baserow#4558)
* Ensure that a string ID is handled when upserting a collaborator field. * Update changelog * Fix serializer check * Create a new CollaboratorField that supports both int, dicts, and string numbers. * Add tests for CollaboratorField * Set required=False as a default * Fix domain for changelog * Add database changelog
1 parent 8391771 commit bedc1b4

File tree

7 files changed

+169
-10
lines changed

7 files changed

+169
-10
lines changed

backend/src/baserow/contrib/database/api/fields/serializers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,40 @@ class UniqueRowValuesSerializer(serializers.Serializer):
447447
values = serializers.ListSerializer(child=serializers.CharField())
448448

449449

450+
@extend_schema_field(OpenApiTypes.INT)
451+
class CollaboratorField(serializers.Field):
452+
"""
453+
A serializer field that accepts an int or a dict with an "id" field.
454+
"""
455+
456+
def to_internal_value(self, data):
457+
if isinstance(data, int) or (isinstance(data, str) and data.isdigit()):
458+
return int(data)
459+
460+
if isinstance(data, dict):
461+
try:
462+
return int(data["id"])
463+
except (KeyError, TypeError, ValueError):
464+
pass
465+
466+
raise serializers.ValidationError(
467+
"Expected an integer or an object with an 'id' field",
468+
code="invalid",
469+
)
470+
471+
def to_representation(self, value):
472+
return value
473+
474+
450475
class CollaboratorSerializer(serializers.Serializer):
451476
id = serializers.IntegerField()
452477
name = serializers.CharField(source="first_name", read_only=True)
453478

454479

480+
class CollaboratorRequestSerializer(serializers.ListSerializer):
481+
child = CollaboratorField()
482+
483+
455484
class AvailableCollaboratorsSerializer(serializers.ListField):
456485
def __init__(self, **kwargs):
457486
kwargs["child"] = CollaboratorSerializer()

backend/src/baserow/contrib/database/fields/field_types.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6744,18 +6744,13 @@ def can_represent_collaborators(self, field):
67446744
return True
67456745

67466746
def get_serializer_field(self, instance, **kwargs):
6747-
required = kwargs.pop("required", False)
6748-
field_serializer = CollaboratorSerializer(
6749-
**{
6750-
"required": required,
6751-
"allow_null": False,
6752-
**kwargs,
6753-
}
6754-
)
6755-
return serializers.ListSerializer(
6756-
child=field_serializer, required=required, **kwargs
6747+
from baserow.contrib.database.api.fields.serializers import (
6748+
CollaboratorRequestSerializer,
67576749
)
67586750

6751+
kwargs.setdefault("required", False)
6752+
return CollaboratorRequestSerializer(**kwargs)
6753+
67596754
def get_search_expression(
67606755
self, field: MultipleCollaboratorsField, queryset: QuerySet
67616756
) -> Expression:
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
from rest_framework import serializers
3+
4+
from baserow.contrib.database.api.fields.serializers import (
5+
CollaboratorField,
6+
CollaboratorRequestSerializer,
7+
)
8+
9+
10+
def test_collaborator_request_serializer_with_list_of_ints():
11+
result = CollaboratorRequestSerializer().to_internal_value([1, 2, 3])
12+
assert result == [1, 2, 3]
13+
14+
15+
def test_collaborator_request_serializer_with_list_of_dicts():
16+
result = CollaboratorRequestSerializer().to_internal_value([{"id": 1}, {"id": 2}])
17+
assert result == [1, 2]
18+
19+
20+
def test_collaborator_request_serializer_with_ints_and_dicts():
21+
serializer = CollaboratorRequestSerializer()
22+
result = serializer.to_internal_value([1, {"id": 2}, "3"])
23+
assert result == [1, 2, 3]
24+
25+
26+
def test_collaborator_field_int():
27+
assert CollaboratorField().to_internal_value(100) == 100
28+
29+
30+
def test_collaborator_field_numeric_string():
31+
assert CollaboratorField().to_internal_value("200") == 200
32+
33+
34+
def test_collaborator_field_dict_with_int_id():
35+
assert CollaboratorField().to_internal_value({"id": 300}) == 300
36+
37+
38+
def test_collaborator_field_dict_with_string_id():
39+
assert CollaboratorField().to_internal_value({"id": "404"}) == 404
40+
41+
42+
def test_collaborator_field_dict_without_id():
43+
with pytest.raises(serializers.ValidationError):
44+
CollaboratorField().to_internal_value({"foo": "bar"})
45+
46+
47+
def test_collaborator_field_invalid_type():
48+
with pytest.raises(serializers.ValidationError):
49+
CollaboratorField().to_internal_value(["foo"])
50+
51+
52+
def test_collaborator_field_non_numeric_string():
53+
with pytest.raises(serializers.ValidationError):
54+
CollaboratorField().to_internal_value("foo bar")

backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ def test_multiple_collaborators_field_type_create(data_fixture):
5050
assert collaborator_field_list[0].id == user.id
5151

5252

53+
@pytest.mark.django_db
54+
@pytest.mark.field_multiple_collaborators
55+
def test_multiple_collaborators_field_type_create_with_int(data_fixture):
56+
user = data_fixture.create_user()
57+
database = data_fixture.create_database_application(user=user, name="Placeholder")
58+
table = data_fixture.create_database_table(name="Example", database=database)
59+
60+
row_handler = RowHandler()
61+
62+
collaborator_field = data_fixture.create_multiple_collaborators_field(
63+
user=user, table=table, name="Collaborator 1"
64+
)
65+
field_id = collaborator_field.db_column
66+
67+
assert MultipleCollaboratorsField.objects.all().first().id == collaborator_field.id
68+
69+
row = row_handler.create_row(user=user, table=table, values={field_id: [user.id]})
70+
assert row.id
71+
collaborator_field_list = getattr(row, field_id).all()
72+
assert len(collaborator_field_list) == 1
73+
assert collaborator_field_list[0].id == user.id
74+
75+
5376
@pytest.mark.django_db
5477
@pytest.mark.field_multiple_collaborators
5578
def test_multiple_collaborators_field_type_update(data_fixture):

backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,3 +833,43 @@ def test_extract_properties_returns_expected_list(path, expected):
833833
result = service_type.extract_properties(path)
834834

835835
assert result == expected
836+
837+
838+
@pytest.mark.django_db
839+
def test_local_baserow_upsert_row_service_dispatch_data_with_collaborators(
840+
data_fixture,
841+
):
842+
user = data_fixture.create_user()
843+
page = data_fixture.create_builder_page(user=user)
844+
integration = data_fixture.create_local_baserow_integration(
845+
application=page.builder, user=user
846+
)
847+
database = data_fixture.create_database_application(
848+
workspace=page.builder.workspace
849+
)
850+
table = TableHandler().create_table_and_fields(
851+
user=user,
852+
database=database,
853+
name=data_fixture.fake.name(),
854+
fields=[
855+
("Collaborators", "multiple_collaborators", {}),
856+
],
857+
)
858+
collaborator_field = table.field_set.get(name="Collaborators")
859+
860+
service = data_fixture.create_local_baserow_upsert_row_service(
861+
integration=integration,
862+
table=table,
863+
)
864+
service_type = service.get_type()
865+
# Simulate a user ID as a string
866+
service.field_mappings.create(field=collaborator_field, value=f'"{user.id}"')
867+
868+
dispatch_context = FakeDispatchContext()
869+
dispatch_values = service_type.resolve_service_formulas(service, dispatch_context)
870+
dispatch_data = service_type.dispatch_data(
871+
service, dispatch_values, dispatch_context
872+
)
873+
874+
collaborators = getattr(dispatch_data["data"], collaborator_field.db_column).all()
875+
assert list(collaborators.values_list("id", flat=True)) == [user.id]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fixed a bug preventing Collaborator field from being upserted by user ID.",
4+
"issue_origin": "github",
5+
"issue_number": 3954,
6+
"domain": "database",
7+
"bullet_points": [],
8+
"created_at": "2026-01-28"
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fixed a bug where the collaborators field couldn't be updated via upsert actions.",
4+
"issue_origin": "github",
5+
"issue_number": 3954,
6+
"domain": "integration",
7+
"bullet_points": [],
8+
"created_at": "2026-01-19"
9+
}

0 commit comments

Comments
 (0)