1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Resolve "[feature] Ability to link to the current table in "Link to table""

This commit is contained in:
Davide Silvestri 2022-06-30 07:00:40 +00:00
parent 5143374025
commit e14a4205d0
20 changed files with 748 additions and 82 deletions

View file

@ -1,14 +0,0 @@
# mypy.ini
[mypy]
python_version = 3.7
exclude = "[a-zA-Z_]+.migrations.|[a-zA-Z_]+.tests.|[a-zA-Z_]+.testing."
allow_redefinition = false
plugins =
mypy_django_plugin.main,
[mypy.plugins.django-stubs]
django_settings_module = "baserow.config.settings.base"

View file

@ -267,14 +267,10 @@ class ForeignKeyAirtableColumnType(AirtableColumnType):
type_options = raw_airtable_column.get("typeOptions", {}) type_options = raw_airtable_column.get("typeOptions", {})
foreign_table_id = type_options.get("foreignTableId") foreign_table_id = type_options.get("foreignTableId")
# Only return a link row field if the foreign table id is not the same as the return LinkRowField(
# current table id because we're not supported link_row fields that point to link_row_table_id=foreign_table_id,
# the same table. link_row_related_field_id=type_options.get("symmetricColumnId"),
if raw_airtable_table["id"] != foreign_table_id: )
return LinkRowField(
link_row_table_id=foreign_table_id,
link_row_related_field_id=type_options.get("symmetricColumnId"),
)
def to_baserow_export_serialized_value( def to_baserow_export_serialized_value(
self, self,

View file

@ -6,7 +6,10 @@ from django.urls import path, include
from django.utils import timezone from django.utils import timezone
from baserow.contrib.database.api.serializers import DatabaseSerializer from baserow.contrib.database.api.serializers import DatabaseSerializer
from baserow.contrib.database.db.schema import safe_django_schema_editor from baserow.contrib.database.db.schema import (
create_model_and_related_tables_without_duplicates,
safe_django_schema_editor,
)
from baserow.contrib.database.fields.dependencies.update_collector import ( from baserow.contrib.database.fields.dependencies.update_collector import (
FieldUpdateCollector, FieldUpdateCollector,
) )
@ -225,7 +228,7 @@ class DatabaseApplicationType(ApplicationType):
add_dependencies=False, add_dependencies=False,
) )
table["_model"] = model table["_model"] = model
schema_editor.create_model(model) create_model_and_related_tables_without_duplicates(schema_editor, model)
# The auto_now_add and auto_now must be disabled for all fields # The auto_now_add and auto_now must be disabled for all fields
# because the export contains correct values and we don't want them # because the export contains correct values and we don't want them

View file

@ -260,6 +260,38 @@ def optional_atomic(atomic=True):
yield yield
def create_model_and_related_tables_without_duplicates(schema_editor, model):
"""
Create a table and any accompanying indexes or unique constraints for
the given `model`.
NOTE: this method is a clone of `schema_editor.create_model` with a change:
it checks if the through table already exists and if it does, it does not try to
create it again (otherwise we'll end up with a table already exists exception).
In this way we can create both sides of the m2m relationship for self-referencing
link_rows when importing data without errors.
"""
sql, params = schema_editor.table_sql(model)
# Prevent using [] as params, in the case a literal '%' is used in the definition
schema_editor.execute(sql, params or None)
# Add any field index and index_together's
schema_editor.deferred_sql.extend(schema_editor._model_indexes_sql(model))
# Make M2M tables
already_created_through_table_name = set()
for field in model._meta.local_many_to_many:
remote_through = field.remote_field.through
db_table = remote_through._meta.db_table
if (
field.remote_field.through._meta.auto_created
and db_table not in already_created_through_table_name
):
schema_editor.create_model(remote_through)
already_created_through_table_name.add(db_table)
@contextlib.contextmanager @contextlib.contextmanager
def safe_django_schema_editor(atomic=True, **kwargs): def safe_django_schema_editor(atomic=True, **kwargs):
# django.db.backends.base.schema.BaseDatabaseSchemaEditor.__exit__ has a bug # django.db.backends.base.schema.BaseDatabaseSchemaEditor.__exit__ has a bug

View file

@ -1000,7 +1000,9 @@ class LinkRowFieldType(FieldType):
] ]
serializer_field_names = ["link_row_table", "link_row_related_field"] serializer_field_names = ["link_row_table", "link_row_related_field"]
serializer_field_overrides = { serializer_field_overrides = {
"link_row_related_field": serializers.PrimaryKeyRelatedField(read_only=True) "link_row_related_field": serializers.PrimaryKeyRelatedField(
read_only=True, required=False
)
} }
api_exceptions_map = { api_exceptions_map = {
LinkRowTableNotProvided: ERROR_LINK_ROW_TABLE_NOT_PROVIDED, LinkRowTableNotProvided: ERROR_LINK_ROW_TABLE_NOT_PROVIDED,
@ -1189,13 +1191,16 @@ class LinkRowFieldType(FieldType):
manytomany_models[instance.table_id] = model manytomany_models[instance.table_id] = model
# Check if the related table model is already in the manytomany_models. # Check if the related table model is already in the manytomany_models.
related_model = manytomany_models.get(instance.link_row_table_id) is_referencing_the_same_table = instance.link_row_table_id == instance.table_id
if is_referencing_the_same_table:
# If we do not have a related table model already we can generate a new one. related_model = model
if not related_model: else:
related_model = instance.link_row_table.get_model( related_model = manytomany_models.get(instance.link_row_table_id)
manytomany_models=manytomany_models # If we do not have a related table model already we can generate a new one.
) if related_model is None:
related_model = instance.link_row_table.get_model(
manytomany_models=manytomany_models
)
instance._related_model = related_model instance._related_model = related_model
related_name = f"reversed_field_{instance.id}" related_name = f"reversed_field_{instance.id}"
@ -1203,16 +1208,21 @@ class LinkRowFieldType(FieldType):
# Try to find the related field in the related model in order to figure out what # Try to find the related field in the related model in order to figure out what
# the related name should be. If the related if is not found that means that it # the related name should be. If the related if is not found that means that it
# has not yet been created. # has not yet been created.
for related_field in related_model._field_objects.values(): def field_is_link_row_related_field(related_field):
if ( return (
isinstance(related_field["field"], self.model_class) isinstance(related_field["field"], self.model_class)
and related_field["field"].link_row_related_field_id and related_field["field"].link_row_related_field_id
and related_field["field"].link_row_related_field_id == instance.id and related_field["field"].link_row_related_field_id == instance.id
): )
related_name = related_field["name"]
# Note that the through model will not be registered with the apps because of if not is_referencing_the_same_table:
# the `DatabaseConfig.prevent_generated_model_for_registering` hack. for related_field in related_model._field_objects.values():
if field_is_link_row_related_field(related_field):
related_name = related_field["name"]
break
# Note that the through model will not be registered with the apps because
# of the `DatabaseConfig.prevent_generated_model_for_registering` hack.
models.ManyToManyField( models.ManyToManyField(
to=related_model, to=related_model,
related_name=related_name, related_name=related_name,
@ -1222,16 +1232,30 @@ class LinkRowFieldType(FieldType):
db_constraint=False, db_constraint=False,
).contribute_to_class(model, field_name) ).contribute_to_class(model, field_name)
model_field = model._meta.get_field(field_name)
through_model = model_field.remote_field.through
if is_referencing_the_same_table:
# manually create the opposite relation on the same through_model.
from_field, to_field = through_model._meta.get_fields()[1:]
models.ManyToManyField(
to="self",
related_name=field_name,
null=True,
blank=True,
symmetrical=False,
through=through_model,
through_fields=(to_field.name, from_field.name),
).contribute_to_class(model, related_name)
# Trigger the newly created pending operations of all the models related to the # Trigger the newly created pending operations of all the models related to the
# created ManyToManyField. They need to be called manually because normally # created ManyToManyField. They need to be called manually because normally
# they are triggered when a new model is registered. Not triggering them # they are triggered when a new model is registered. Not triggering them
# can cause a memory leak because everytime a table model is generated, it will # can cause a memory leak because everytime a table model is generated, it will
# register new pending operations. # register new pending operations.
apps = model._meta.apps apps = model._meta.apps
model_field = model._meta.get_field(field_name)
apps.do_pending_operations(model) apps.do_pending_operations(model)
apps.do_pending_operations(related_model) apps.do_pending_operations(related_model)
apps.do_pending_operations(model_field.remote_field.through) apps.do_pending_operations(through_model)
apps.clear_cache() apps.clear_cache()
def prepare_values(self, values, user): def prepare_values(self, values, user):
@ -1309,7 +1333,7 @@ class LinkRowFieldType(FieldType):
table so a reversed lookup can be done by the user. table so a reversed lookup can be done by the user.
""" """
if field.link_row_related_field: if field.link_row_related_field or field.table == field.link_row_table:
return return
related_field_name = self.find_next_unused_related_field_name(field) related_field_name = self.find_next_unused_related_field_name(field)
@ -1347,7 +1371,10 @@ class LinkRowFieldType(FieldType):
to_model_field, to_model_field,
user, user,
): ):
if not isinstance(to_field, self.model_class): if (
not isinstance(to_field, self.model_class)
and from_field.link_row_related_field is not None
):
# If we are not going to convert to another manytomany field the # If we are not going to convert to another manytomany field the
# related field can be deleted. # related field can be deleted.
from_field.link_row_related_field.delete() from_field.link_row_related_field.delete()
@ -1359,13 +1386,35 @@ class LinkRowFieldType(FieldType):
# If the table has changed we have to change the following data in the # If the table has changed we have to change the following data in the
# related field # related field
related_field_name = self.find_next_unused_related_field_name(to_field) related_field_name = self.find_next_unused_related_field_name(to_field)
from_field.link_row_related_field.name = related_field_name
from_field.link_row_related_field.table = to_field.link_row_table if from_field.link_row_related_field is None:
from_field.link_row_related_field.link_row_table = to_field.table # we need to create the missing link_row_related_field
from_field.link_row_related_field.order = self.model_class.get_last_order( to_field.link_row_related_field = FieldHandler().create_field(
to_field.link_row_table user=user,
) table=to_field.link_row_table,
from_field.link_row_related_field.save() type_name=self.type,
name=related_field_name,
do_schema_change=False,
link_row_table=to_field.table,
link_row_related_field=to_field,
link_row_relation_id=to_field.link_row_relation_id,
)
to_field.save()
elif (
# delete the previous field that is not needed anymore
# since we are referencing the same table now
to_field.link_row_related_field is not None
and to_field.table_id == to_field.link_row_table_id
):
to_field.link_row_related_field.delete()
else:
from_field.link_row_related_field.name = related_field_name
from_field.link_row_related_field.table = to_field.link_row_table
from_field.link_row_related_field.link_row_table = to_field.table
from_field.link_row_related_field.order = (
self.model_class.get_last_order(to_field.link_row_table)
)
from_field.link_row_related_field.save()
def after_update( def after_update(
self, self,
@ -1383,8 +1432,10 @@ class LinkRowFieldType(FieldType):
field into the related table. field into the related table.
""" """
if not isinstance(from_field, self.model_class) and isinstance( if (
to_field, self.model_class not isinstance(from_field, self.model_class)
and isinstance(to_field, self.model_class)
and to_field.table != to_field.link_row_table
): ):
related_field_name = self.find_next_unused_related_field_name(to_field) related_field_name = self.find_next_unused_related_field_name(to_field)
to_field.link_row_related_field = FieldHandler().create_field( to_field.link_row_related_field = FieldHandler().create_field(
@ -1404,7 +1455,8 @@ class LinkRowFieldType(FieldType):
After the field has been deleted we also need to delete the related field. After the field has been deleted we also need to delete the related field.
""" """
field.link_row_related_field.delete() if field.link_row_related_field is not None:
field.link_row_related_field.delete()
def random_value(self, instance, fake, cache): def random_value(self, instance, fake, cache):
""" """
@ -1510,8 +1562,11 @@ class LinkRowFieldType(FieldType):
): ):
getattr(row, field_name).set(value) getattr(row, field_name).set(value)
def get_other_fields_to_trash_restore_always_together(self, field) -> List[Any]: def get_other_fields_to_trash_restore_always_together(self, field) -> List[Field]:
return [field.link_row_related_field] fields = []
if field.link_row_related_field is not None:
fields.append(field.link_row_related_field)
return fields
def to_baserow_formula_type(self, field) -> BaserowFormulaType: def to_baserow_formula_type(self, field) -> BaserowFormulaType:
primary_field = field.get_related_primary_field() primary_field = field.get_related_primary_field()

View file

@ -147,6 +147,8 @@ class FieldDependencyExtractingVisitor(
if primary_field_in_other_table is None: if primary_field_in_other_table is None:
return [] return []
else: else:
if primary_field_in_other_table.id == source_field.id:
raise SelfReferenceFieldDependencyError()
return [ return [
FieldDependency( FieldDependency(
dependant=source_field, dependant=source_field,
@ -171,6 +173,8 @@ class FieldDependencyExtractingVisitor(
), ),
] ]
else: else:
if target_field.id == source_field.id:
raise SelfReferenceFieldDependencyError()
# We found all the fields correctly and they are valid so setup the # We found all the fields correctly and they are valid so setup the
# dep to the other table via the link row field at the target field. # dep to the other table via the link row field at the target field.
return [ return [

View file

@ -743,13 +743,24 @@ class RowHandler:
value_column = None value_column = None
row_column = None row_column = None
model_field = model._meta.get_field(field_name)
is_referencing_the_same_table = (
model_field.model == model_field.related_model
)
# Figure out which field in the many to many through table holds the row # Figure out which field in the many to many through table holds the row
# value and which on contains the value. # value and which on contains the value.
for field in through_fields: for field in through_fields:
if type(field) is not ForeignKey: if type(field) is not ForeignKey:
continue continue
if field.remote_field.model == model: if is_referencing_the_same_table:
# django creates 'from_tableXmodel' and 'to_tableXmodel'
# columns for self-referencing many_to_many relations.
row_column = field.get_attname_column()[1]
value_column = row_column.replace("from", "to")
break
elif field.remote_field.model == model:
row_column = field.get_attname_column()[1] row_column = field.get_attname_column()[1]
else: else:
value_column = field.get_attname_column()[1] value_column = field.get_attname_column()[1]
@ -939,6 +950,11 @@ class RowHandler:
value_column = None value_column = None
row_column = None row_column = None
model_field = model._meta.get_field(field_name)
is_referencing_the_same_table = (
model_field.model == model_field.related_model
)
# Figure out which field in the many to many through table holds the row # Figure out which field in the many to many through table holds the row
# value and which one contains the value. # value and which one contains the value.
for field in through_fields: for field in through_fields:
@ -947,7 +963,14 @@ class RowHandler:
row_ids_change_m2m_per_field[field_name].add(row.id) row_ids_change_m2m_per_field[field_name].add(row.id)
if field.remote_field.model == model: if is_referencing_the_same_table:
# django creates 'from_tableXmodel' and 'to_tableXmodel'
# columns for self-referencing many_to_many relations.
row_column = field.get_attname_column()[1]
row_column_name = row_column
value_column = row_column.replace("from", "to")
break
elif field.remote_field.model == model:
row_column = field.get_attname_column()[1] row_column = field.get_attname_column()[1]
row_column_name = row_column row_column_name = row_column
else: else:

View file

@ -350,12 +350,11 @@ class RowsTrashableItemType(TrashableItemType):
def restore(self, trashed_item, trash_entry: TrashEntry): def restore(self, trashed_item, trash_entry: TrashEntry):
table = self._get_table(trashed_item.table_id) table = self._get_table(trashed_item.table_id)
table_model = self._get_table_model(trashed_item.table_id) table_model = self._get_table_model(trashed_item.table_id)
rows_to_restore = table_model.objects_and_trash.filter( rows_to_restore_queryset = table_model.objects_and_trash.filter(
id__in=trashed_item.row_ids id__in=trashed_item.row_ids
).enhance_by_fields() )
for row in rows_to_restore: rows_to_restore_queryset.update(trashed=False)
row.trashed = False rows_to_restore = rows_to_restore_queryset.enhance_by_fields()
table_model.objects_and_trash.bulk_update(rows_to_restore, ["trashed"])
trashed_item.delete() trashed_item.delete()
field_cache = FieldCache() field_cache = FieldCache()

View file

@ -293,9 +293,9 @@ class ViewType(
pass pass
if self.can_sort: if self.can_sort:
for view_decoration in sortings: for view_sorting in sortings:
try: try:
view_sort_copy = view_decoration.copy() view_sort_copy = view_sorting.copy()
view_sort_id = view_sort_copy.pop("id") view_sort_id = view_sort_copy.pop("id")
view_sort_copy["field_id"] = id_mapping["database_fields"][ view_sort_copy["field_id"] = id_mapping["database_fields"][
view_sort_copy["field_id"] view_sort_copy["field_id"]

View file

@ -665,6 +665,7 @@ def test_airtable_import_foreign_key_column(data_fixture, api_client):
== [1, 2] == [1, 2]
) )
# link to same table row
airtable_field = { airtable_field = {
"id": "fldQcEaGEe7xuhUEuPL", "id": "fldQcEaGEe7xuhUEuPL",
"name": "Link to Users", "name": "Link to Users",
@ -682,8 +683,10 @@ def test_airtable_import_foreign_key_column(data_fixture, api_client):
) = airtable_column_type_registry.from_airtable_column_to_serialized( ) = airtable_column_type_registry.from_airtable_column_to_serialized(
{"id": "tblRpq315qnnIcg5IjI"}, airtable_field, UTC {"id": "tblRpq315qnnIcg5IjI"}, airtable_field, UTC
) )
assert baserow_field is None assert isinstance(baserow_field, LinkRowField)
assert airtable_column_type is None assert isinstance(airtable_column_type, ForeignKeyAirtableColumnType)
assert baserow_field.link_row_table_id == "tblRpq315qnnIcg5IjI"
assert baserow_field.link_row_related_field_id == "fldQcEaGEe7xuhUEuPL"
@pytest.mark.django_db @pytest.mark.django_db

View file

@ -210,7 +210,7 @@ def test_to_baserow_database_export():
assert baserow_database_export["tables"][1]["id"] == "tbl7glLIGtH8C8zGCzb" assert baserow_database_export["tables"][1]["id"] == "tbl7glLIGtH8C8zGCzb"
assert baserow_database_export["tables"][1]["name"] == "Data" assert baserow_database_export["tables"][1]["name"] == "Data"
assert baserow_database_export["tables"][1]["order"] == 1 assert baserow_database_export["tables"][1]["order"] == 1
assert len(baserow_database_export["tables"][1]["fields"]) == 23 assert len(baserow_database_export["tables"][1]["fields"]) == 24
# We don't have to check all the fields and rows, just a single one, because we have # We don't have to check all the fields and rows, just a single one, because we have
# separate tests for mapping the Airtable fields and values to Baserow. # separate tests for mapping the Airtable fields and values to Baserow.

View file

@ -81,6 +81,86 @@ def test_batch_create_rows_link_row_field(api_client, data_fixture):
assert getattr(rows[2], f"field_{link_field.id}").count() == 0 assert getattr(rows[2], f"field_{link_field.id}").count() == 0
@pytest.mark.django_db
@pytest.mark.field_link_row
@pytest.mark.api_rows
def test_batch_create_rows_link_same_table_row_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
primary_field = data_fixture.create_text_field(
primary=True,
name="Primary",
table=table,
)
link_field = FieldHandler().create_field(
user, table, "link_row", link_row_table=table, name="Link"
)
model = table.get_model()
row_1 = model.objects.create(**{f"field_{primary_field.id}": "Row 1"})
row_2 = model.objects.create(**{f"field_{primary_field.id}": "Row 2"})
row_3 = model.objects.create(**{f"field_{primary_field.id}": "Row 3"})
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
f"field_{primary_field.id}": "Row 4",
f"field_{link_field.id}": [row_3.id],
},
{
f"field_{primary_field.id}": "Row 5",
f"field_{link_field.id}": [row_2.id, row_1.id],
},
{
f"field_{primary_field.id}": "Row 6",
f"field_{link_field.id}": [],
},
]
}
expected_response_body = {
"items": [
{
"id": 4,
f"field_{primary_field.id}": "Row 4",
f"field_{link_field.id}": [{"id": row_3.id, "value": "Row 3"}],
"order": "2.00000000000000000000",
},
{
"id": 5,
f"field_{primary_field.id}": "Row 5",
f"field_{link_field.id}": [
{"id": row_1.id, "value": "Row 1"},
{"id": row_2.id, "value": "Row 2"},
],
"order": "3.00000000000000000000",
},
{
"id": 6,
f"field_{primary_field.id}": "Row 6",
f"field_{link_field.id}": [],
"order": "4.00000000000000000000",
},
]
}
response = api_client.post(
url,
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
assert response.json() == expected_response_body
rows = model.objects.all()[3:]
assert getattr(rows[0], f"field_{link_field.id}").count() == 1
assert getattr(rows[1], f"field_{link_field.id}").count() == 2
assert getattr(rows[2], f"field_{link_field.id}").count() == 0
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row @pytest.mark.field_link_row
@pytest.mark.api_rows @pytest.mark.api_rows
@ -158,3 +238,79 @@ def test_batch_update_rows_link_row_field(api_client, data_fixture):
assert getattr(row_1, f"field_{link_field.id}").count() == 1 assert getattr(row_1, f"field_{link_field.id}").count() == 1
assert getattr(row_2, f"field_{link_field.id}").count() == 2 assert getattr(row_2, f"field_{link_field.id}").count() == 2
assert getattr(row_3, f"field_{link_field.id}").count() == 0 assert getattr(row_3, f"field_{link_field.id}").count() == 0
@pytest.mark.django_db
@pytest.mark.field_link_row
@pytest.mark.api_rows
def test_batch_update_rows_link_same_table_row_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
primary_field = data_fixture.create_text_field(
primary=True,
name="Primary",
table=table,
)
link_field = FieldHandler().create_field(
user, table, "link_row", link_row_table=table, name="Link"
)
model = table.get_model()
row_1 = model.objects.create(**{f"field_{primary_field.id}": "Row 1"})
row_2 = model.objects.create(**{f"field_{primary_field.id}": "Row 2"})
row_3 = model.objects.create(**{f"field_{primary_field.id}": "Row 3"})
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
"id": row_1.id,
f"field_{link_field.id}": [row_3.id],
},
{
"id": row_2.id,
f"field_{link_field.id}": [row_3.id, row_2.id],
},
{
"id": row_3.id,
f"field_{link_field.id}": [],
},
]
}
expected_response_body = {
"items": [
{
"id": row_1.id,
f"field_{primary_field.id}": "Row 1",
f"field_{link_field.id}": [{"id": row_3.id, "value": "Row 3"}],
"order": "1.00000000000000000000",
},
{
"id": row_2.id,
f"field_{primary_field.id}": "Row 2",
f"field_{link_field.id}": [
{"id": row_2.id, "value": "Row 2"},
{"id": row_3.id, "value": "Row 3"},
],
"order": "1.00000000000000000000",
},
{
"id": row_3.id,
f"field_{primary_field.id}": "Row 3",
f"field_{link_field.id}": [],
"order": "1.00000000000000000000",
},
]
}
response = api_client.patch(
url,
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
assert response.json() == expected_response_body
assert getattr(row_1, f"field_{link_field.id}").count() == 1
assert getattr(row_2, f"field_{link_field.id}").count() == 2
assert getattr(row_3, f"field_{link_field.id}").count() == 0

View file

@ -187,6 +187,7 @@ def test_dependencies_for_triple_lookup(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_dependencies_for_link_row_link_row_self_reference(data_fixture): def test_dependencies_for_link_row_link_row_self_reference(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
table_a = data_fixture.create_database_table(user=user) table_a = data_fixture.create_database_table(user=user)
@ -199,8 +200,6 @@ def test_dependencies_for_link_row_link_row_self_reference(data_fixture):
name="self", name="self",
link_row_table=table_a.id, link_row_table=table_a.id,
) )
# todo remove once the self referencing link row field MR is merged.
table_a_self_link.link_row_related_field.delete()
assert when_field_updated(table_a_primary) == causes( assert when_field_updated(table_a_primary) == causes(
a_field_update_for(field=table_a_self_link, via=[table_a_self_link]) a_field_update_for(field=table_a_self_link, via=[table_a_self_link])
) )
@ -236,6 +235,7 @@ def a_field_update_for(field, via, then=None):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_get_dependant_fields_with_type(data_fixture): def test_get_dependant_fields_with_type(data_fixture):
table = data_fixture.create_database_table() table = data_fixture.create_database_table()
text_field_1 = data_fixture.create_text_field(table=table) text_field_1 = data_fixture.create_text_field(table=table)

View file

@ -34,7 +34,7 @@ def test_alter_boolean_field_column_type(data_fixture):
for value in mapping.keys(): for value in mapping.keys():
model.objects.create(**{f"field_{field.id}": value}) model.objects.create(**{f"field_{field.id}": value})
# Change the field type to a number and test if the values have been changed. # Change the field type to a boolean and test if the values have been changed.
field = handler.update_field(user=user, field=field, new_type_name="boolean") field = handler.update_field(user=user, field=field, new_type_name="boolean")
model = table.get_model() model = table.get_model()

View file

@ -1,25 +1,27 @@
import pytest
from io import BytesIO from io import BytesIO
import pytest
from django.apps.registry import apps
from django.db import connections
from django.shortcuts import reverse
from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST
from django.shortcuts import reverse
from django.db import connections
from django.apps.registry import apps
from baserow.core.handler import CoreHandler
from baserow.contrib.database.fields.models import Field, TextField, LinkRowField
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.exceptions import ( from baserow.contrib.database.fields.exceptions import (
LinkRowTableNotInSameDatabase, LinkRowTableNotInSameDatabase,
LinkRowTableNotProvided, LinkRowTableNotProvided,
) )
from baserow.contrib.database.fields.dependencies.exceptions import (
SelfReferenceFieldDependencyError,
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import Field, TextField, LinkRowField
from baserow.contrib.database.rows.handler import RowHandler from baserow.contrib.database.rows.handler import RowHandler
from baserow.core.handler import CoreHandler
from baserow.core.trash.handler import TrashHandler from baserow.core.trash.handler import TrashHandler
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_call_apps_registry_pending_operations(data_fixture): def test_call_apps_registry_pending_operations(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder") database = data_fixture.create_database_application(user=user, name="Placeholder")
@ -43,6 +45,7 @@ def test_call_apps_registry_pending_operations(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_type(data_fixture): def test_link_row_field_type(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder") database = data_fixture.create_database_application(user=user, name="Placeholder")
@ -226,6 +229,7 @@ def test_link_row_field_type(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_type_rows(data_fixture): def test_link_row_field_type_rows(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder") database = data_fixture.create_database_application(user=user, name="Placeholder")
@ -365,6 +369,7 @@ def test_link_row_field_type_rows(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_enhance_queryset(data_fixture, django_assert_num_queries): def test_link_row_enhance_queryset(data_fixture, django_assert_num_queries):
user = data_fixture.create_user() user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user, name="Placeholder") database = data_fixture.create_database_application(user=user, name="Placeholder")
@ -421,6 +426,7 @@ def test_link_row_enhance_queryset(data_fixture, django_assert_num_queries):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_type_api_views(api_client, data_fixture): def test_link_row_field_type_api_views(api_client, data_fixture):
user, token = data_fixture.create_user_and_token( user, token = data_fixture.create_user_and_token(
email="test@test.nl", password="password", first_name="Test1" email="test@test.nl", password="password", first_name="Test1"
@ -591,6 +597,7 @@ def test_link_row_field_type_api_views(api_client, data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_type_api_row_views(api_client, data_fixture): def test_link_row_field_type_api_row_views(api_client, data_fixture):
user, token = data_fixture.create_user_and_token() user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user, name="Placeholder") database = data_fixture.create_database_application(user=user, name="Placeholder")
@ -746,6 +753,7 @@ def test_link_row_field_type_api_row_views(api_client, data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_import_export_link_row_field(data_fixture): def test_import_export_link_row_field(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
imported_group = data_fixture.create_group(user=user) imported_group = data_fixture.create_group(user=user)
@ -817,6 +825,7 @@ def test_import_export_link_row_field(data_fixture):
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_creating_a_linked_row_pointing_at_trashed_row_works_but_does_not_display( def test_creating_a_linked_row_pointing_at_trashed_row_works_but_does_not_display(
data_fixture, api_client data_fixture, api_client
): ):
@ -918,6 +927,7 @@ def test_creating_a_linked_row_pointing_at_trashed_row_works_but_does_not_displa
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_change_type_to_link_row_field_when_field_with_same_related_name_already_exists( def test_change_type_to_link_row_field_when_field_with_same_related_name_already_exists(
data_fixture, data_fixture,
): ):
@ -937,7 +947,7 @@ def test_change_type_to_link_row_field_when_field_with_same_related_name_already
model.objects.create(**{f"field_{field.id}": "9223372036854775807"}) model.objects.create(**{f"field_{field.id}": "9223372036854775807"})
model.objects.create(**{f"field_{field.id}": "100"}) model.objects.create(**{f"field_{field.id}": "100"})
# Change the field type to a number and test if the values have been changed. # Change the field type to a link_row and test if names are changed corectly.
new_link_row_field = handler.update_field( new_link_row_field = handler.update_field(
user=user, user=user,
field=field, field=field,
@ -954,6 +964,7 @@ def test_change_type_to_link_row_field_when_field_with_same_related_name_already
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.field_link_row
def test_change_link_row_related_table_when_field_with_related_name_exists( def test_change_link_row_related_table_when_field_with_related_name_exists(
data_fixture, data_fixture,
): ):
@ -973,7 +984,7 @@ def test_change_link_row_related_table_when_field_with_related_name_exists(
user, table, "link_row", link_row_table=first_related_table, name="Link" user, table, "link_row", link_row_table=first_related_table, name="Link"
) )
# Change the field type to a number and test if the values have been changed. # Change the field type to a link_row and test if the name have been changed.
handler.update_field( handler.update_field(
user=user, user=user,
field=link_row, field=link_row,
@ -988,3 +999,246 @@ def test_change_link_row_related_table_when_field_with_related_name_exists(
) )
assert names == ["Table", "Table - Link"] assert names == ["Table", "Table - Link"]
assert LinkRowField.objects.count() == 2 assert LinkRowField.objects.count() == 2
@pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_can_link_same_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user, name="Table")
field_handler = FieldHandler()
field = data_fixture.create_text_field(
table=table, order=1, primary=True, name="Name"
)
link_row = field_handler.create_field(
user, table, "link_row", link_row_table=table, name="Link"
)
link_row.refresh_from_db()
assert link_row.link_row_related_field is None
field_names = Field.objects.filter(table=table).values_list("name", flat=True)
assert list(field_names) == ["Name", "Link"]
row_handler = RowHandler()
row_1 = row_handler.create_row(
user=user,
table=table,
values={
f"field_{field.id}": "Tesla",
},
)
row_2 = row_handler.create_row(
user=user,
table=table,
values={
f"field_{field.id}": "Amazon",
f"field_{link_row.id}": [row_1.id],
},
)
assert getattr(row_2, f"field_{link_row.id}").count() == 1
assert getattr(row_2, f"field_{link_row.id}").all()[0].id == row_1.id
url = reverse(
"api:database:rows:item",
kwargs={"table_id": table.id, "row_id": row_1.id},
)
response = api_client.patch(
url,
{f"field_{link_row.id}": [row_1.id, row_2.id]},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[f"field_{link_row.id}"] == [
{"id": row_1.id, "value": "Tesla"},
{"id": row_2.id, "value": "Amazon"},
]
# can be trashed and restored
field_handler.delete_field(user, link_row)
assert link_row.trashed is True
TrashHandler().restore_item(user, "field", link_row.id)
link_row.refresh_from_db()
assert link_row.trashed is False
@pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_field_can_link_same_table_and_another_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table_a = data_fixture.create_database_table(user=user)
table_b = data_fixture.create_database_table(user=user, database=table_a.database)
grid = data_fixture.create_grid_view(user, table=table_a)
table_a_primary = data_fixture.create_text_field(
user, table=table_a, primary=True, name="table a pk"
)
table_b_primary = data_fixture.create_text_field(
user, table=table_b, primary=True, name="table a pk"
)
field_handler = FieldHandler()
table_a_self_link = field_handler.create_field(
user, table_a, "link_row", link_row_table=table_a, name="A->A"
)
link_field = field_handler.create_field(
user, table_b, "link_row", link_row_table=table_a, name="B->A"
)
row_handler = RowHandler()
table_a_row_1 = row_handler.create_row(
user=user,
table=table_a,
values={
f"field_{table_a_primary.id}": "Tesla",
},
)
table_a_row_2 = row_handler.create_row(
user=user,
table=table_a,
values={
f"field_{table_a_primary.id}": "Amazon",
f"field_{table_a_self_link.id}": [table_a_row_1.id],
},
)
table_b_row_1 = row_handler.create_row(
user=user,
table=table_b,
values={
f"field_{table_b_primary.id}": "Amazon",
f"field_{link_field.id}": [table_a_row_1.id],
},
)
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid.id})
response = api_client.get(url, **{"HTTP_AUTHORIZATION": f"JWT {token}"})
response_json = response.json()
assert response.status_code == HTTP_200_OK
@pytest.mark.django_db
@pytest.mark.field_link_row
def test_link_row_can_change_link_from_same_table_to_another_table_and_back(
api_client, data_fixture
):
user, token = data_fixture.create_user_and_token()
table_a = data_fixture.create_database_table(user=user)
table_b = data_fixture.create_database_table(user=user, database=table_a.database)
table_a_primary = data_fixture.create_text_field(
user, table=table_a, primary=True, name="table a pk"
)
table_b_primary = data_fixture.create_text_field(
user, table=table_b, primary=True, name="table b pk"
)
grid_a = data_fixture.create_grid_view(user, table=table_a)
grid_b = data_fixture.create_grid_view(user, table=table_b)
field_handler = FieldHandler()
table_a_link = field_handler.create_field(
user, table_a, "link_row", link_row_table=table_a, name="A->A"
)
row_handler = RowHandler()
table_a_row_1 = row_handler.create_row(
user=user,
table=table_a,
values={
f"field_{table_a_primary.id}": "Tesla",
},
)
table_a_row_2 = row_handler.create_row(
user=user,
table=table_a,
values={
f"field_{table_a_primary.id}": "Amazon",
f"field_{table_a_link.id}": [table_a_row_1.id],
},
)
table_b_row_1 = row_handler.create_row(
user=user,
table=table_b,
values={
f"field_{table_b_primary.id}": "Jeff",
},
)
table_a_link = field_handler.update_field(
user, table_a_link, link_row_table=table_b, name="A->B"
)
# both grid views must be accessible
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid_a.id})
response = api_client.get(url, **{"HTTP_AUTHORIZATION": f"JWT {token}"})
response_json = response.json()
assert response.status_code == HTTP_200_OK
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid_b.id})
response = api_client.get(url, **{"HTTP_AUTHORIZATION": f"JWT {token}"})
response_json = response.json()
assert response.status_code == HTTP_200_OK
names = list(Field.objects.filter(table=table_b).values_list("name", flat=True))
assert len(names) == 2
names = list(Field.objects.filter(table=table_a).values_list("name", flat=True))
assert names == ["table a pk", "A->B"]
# back to the original
table_a_link = field_handler.update_field(
user, table_a_link, link_row_table=table_a, name="A->A again"
)
# both grid views must be accessible
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid_a.id})
response = api_client.get(url, **{"HTTP_AUTHORIZATION": f"JWT {token}"})
assert response.status_code == HTTP_200_OK
url = reverse("api:database:views:grid:list", kwargs={"view_id": grid_b.id})
response = api_client.get(url, **{"HTTP_AUTHORIZATION": f"JWT {token}"})
assert response.status_code == HTTP_200_OK
names = list(Field.objects.filter(table=table_b).values_list("name", flat=True))
assert names == ["table b pk"]
names = list(Field.objects.filter(table=table_a).values_list("name", flat=True))
assert names == ["table a pk", "A->A again"]
@pytest.mark.django_db
@pytest.mark.field_link_row
def test_lookup_field_cannot_self_reference_itself_via_same_table_link_row(
api_client, data_fixture
):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user, name="Table")
field_handler = FieldHandler()
field = data_fixture.create_text_field(
table=table, order=1, primary=True, name="Name"
)
link_row = field_handler.create_field(
user, table, "link_row", link_row_table=table, name="Link"
)
lookup = field_handler.create_field(
user,
table,
"lookup",
through_field_id=link_row.id,
target_field_id=field.id,
name="Lookup",
)
link_row.refresh_from_db()
assert link_row.link_row_related_field is None
field_names = Field.objects.filter(table=table).values_list("name", flat=True)
assert list(field_names) == ["Name", "Link", "Lookup"]
with pytest.raises(SelfReferenceFieldDependencyError):
field_handler.update_field(
user,
lookup,
name="Lookup self",
through_field_id=link_row.id,
target_field_id=lookup.id,
)

View file

@ -642,6 +642,14 @@ def test_can_undo_redo_updating_row(data_fixture):
link_row_table=table_manufacturer, link_row_table=table_manufacturer,
) )
alternative_car_link_row_field = FieldHandler().create_field(
user=user,
table=table_car,
type_name="link_row",
name="alternative_car",
link_row_table=table_car,
)
multiple_select_field = data_fixture.create_multiple_select_field(table=table_car) multiple_select_field = data_fixture.create_multiple_select_field(table=table_car)
select_option_1 = SelectOption.objects.create( select_option_1 = SelectOption.objects.create(
field=multiple_select_field, field=multiple_select_field,
@ -736,6 +744,7 @@ def test_can_undo_redo_updating_row(data_fixture):
f"field_{available_field.id}": False, f"field_{available_field.id}": False,
f"field_{fuel_type_option_field.id}": option_gasoline.id, f"field_{fuel_type_option_field.id}": option_gasoline.id,
manufacturer_link_row_field.id: [alfa_manufacturer.id], manufacturer_link_row_field.id: [alfa_manufacturer.id],
alternative_car_link_row_field.id: [car.id],
year_of_manifacture.id: "2015-09-01", year_of_manifacture.id: "2015-09-01",
multiple_select_field.id: [select_option_3.id], multiple_select_field.id: [select_option_3.id],
} }
@ -758,6 +767,10 @@ def test_can_undo_redo_updating_row(data_fixture):
car, f"field_{manufacturer_link_row_field.id}" car, f"field_{manufacturer_link_row_field.id}"
).values_list("id", flat=True) ).values_list("id", flat=True)
assert list(car_manufacturer) == [alfa_manufacturer.id] assert list(car_manufacturer) == [alfa_manufacturer.id]
car_alternatives = getattr(
car, f"field_{alternative_car_link_row_field.id}"
).values_list("id", flat=True)
assert list(car_alternatives) == [car.id]
assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2015-09-01" assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2015-09-01"
options = getattr(car, f"field_{multiple_select_field.id}").values_list( options = getattr(car, f"field_{multiple_select_field.id}").values_list(
"id", flat=True "id", flat=True
@ -788,6 +801,10 @@ def test_can_undo_redo_updating_row(data_fixture):
car, f"field_{manufacturer_link_row_field.id}" car, f"field_{manufacturer_link_row_field.id}"
).values_list("id", flat=True) ).values_list("id", flat=True)
assert list(car_manufacturer) == [tesla_manufacturer.id] assert list(car_manufacturer) == [tesla_manufacturer.id]
car_alternatives = getattr(
car, f"field_{alternative_car_link_row_field.id}"
).values_list("id", flat=True)
assert list(car_alternatives) == []
assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2018-01-01" assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2018-01-01"
assert not getattr(car, "field_9999", None) assert not getattr(car, "field_9999", None)
options = getattr(car, f"field_{multiple_select_field.id}").values_list( options = getattr(car, f"field_{multiple_select_field.id}").values_list(
@ -819,6 +836,10 @@ def test_can_undo_redo_updating_row(data_fixture):
car, f"field_{manufacturer_link_row_field.id}" car, f"field_{manufacturer_link_row_field.id}"
).values_list("id", flat=True) ).values_list("id", flat=True)
assert list(car_manufacturer) == [alfa_manufacturer.id] assert list(car_manufacturer) == [alfa_manufacturer.id]
car_alternatives = getattr(
car, f"field_{alternative_car_link_row_field.id}"
).values_list("id", flat=True)
assert list(car_alternatives) == [car.id]
assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2015-09-01" assert str(getattr(car, f"field_{year_of_manifacture.id}")) == "2015-09-01"
options = getattr(car, f"field_{multiple_select_field.id}").values_list( options = getattr(car, f"field_{multiple_select_field.id}").values_list(
"id", flat=True "id", flat=True

View file

@ -19,6 +19,7 @@ def test_import_export_database(data_fixture):
formula=f"field('{text_field.name}')", formula=f"field('{text_field.name}')",
formula_type="text", formula_type="text",
) )
data_fixture.create_link_row_field(table=table, link_row_table=table)
view = data_fixture.create_grid_view(table=table) view = data_fixture.create_grid_view(table=table)
data_fixture.create_view_filter(view=view, field=text_field, value="Test") data_fixture.create_view_filter(view=view, field=text_field, value="Test")
data_fixture.create_view_sort(view=view, field=text_field) data_fixture.create_view_sort(view=view, field=text_field)
@ -57,7 +58,7 @@ def test_import_export_database(data_fixture):
assert imported_table.id != table.id assert imported_table.id != table.id
assert imported_table.name == table.name assert imported_table.name == table.name
assert imported_table.order == table.order assert imported_table.order == table.order
assert imported_table.field_set.all().count() == 2 assert imported_table.field_set.all().count() == 3
assert imported_table.view_set.all().count() == 1 assert imported_table.view_set.all().count() == 1
imported_view = imported_table.view_set.all().first() imported_view = imported_table.view_set.all().first()

View file

@ -3059,6 +3059,137 @@ def test_link_row_has_filter_type(data_fixture):
assert row_with_all_relations.id in ids assert row_with_all_relations.id in ids
@pytest.mark.django_db
def test_link_row_reference_same_table_has_filter_type(data_fixture):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
table = data_fixture.create_database_table(database=database)
primary_field = data_fixture.create_text_field(table=table)
grid_view = data_fixture.create_grid_view(table=table)
field_handler = FieldHandler()
link_row_field = field_handler.create_field(
user=user,
table=table,
type_name="link_row",
name="Test",
link_row_table=table,
)
row_handler = RowHandler()
model = table.get_model()
row_0 = row_handler.create_row(
user=user,
table=table,
model=model,
values={
f"field_{primary_field.id}": "Row 0",
},
)
row_1 = row_handler.create_row(
user=user,
table=table,
model=model,
values={
f"field_{primary_field.id}": "Row 1",
f"field_{link_row_field.id}": [row_0.id],
},
)
row_2 = row_handler.create_row(
user=user,
table=table,
model=model,
values={
f"field_{primary_field.id}": "Row 2",
f"field_{link_row_field.id}": [row_0.id, row_1.id],
},
)
row_3 = row_handler.create_row(
user=user,
table=table,
model=model,
values={
f"field_{primary_field.id}": "Row 3",
f"field_{link_row_field.id}": [row_2.id],
},
)
row_with_all_relations = row_handler.create_row(
user=user,
table=table,
model=model,
values={
f"field_{primary_field.id}": "Row 4",
f"field_{link_row_field.id}": [
row_0.id,
row_1.id,
row_2.id,
row_3.id,
],
},
)
handler = ViewHandler()
view_filter = data_fixture.create_view_filter(
view=grid_view,
field=link_row_field,
type="link_row_has",
value=f"",
)
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 5
view_filter.value = "not_number"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 5
view_filter.value = "-1"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 0
view_filter.value = f"{row_0.id}"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 3
assert row_1.id in ids
assert row_2.id in ids
assert row_with_all_relations.id in ids
view_filter.value = f"{row_2.id}"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 2
assert row_3.id in ids
assert row_with_all_relations.id in ids
view_filter.value = f"{row_3.id}"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1
assert row_with_all_relations.id in ids
# Chaining filters should also work
# creating a second filter for the same field
data_fixture.create_view_filter(
view=grid_view,
field=link_row_field,
type="link_row_has",
value=f"{row_1.id}",
)
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1
assert row_with_all_relations.id in ids
# Changing the view to use "OR" for multiple filters
handler.update_view(user=user, view=grid_view, filter_type="OR")
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 2
assert row_2.id in ids
assert row_with_all_relations.id in ids
@pytest.mark.django_db @pytest.mark.django_db
def test_link_row_has_not_filter_type(data_fixture): def test_link_row_has_not_filter_type(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()

View file

@ -20,6 +20,7 @@ For example:
* Added multi-cell clearing via backspace key (delete on Mac). * Added multi-cell clearing via backspace key (delete on Mac).
* Added API exception registry that allows plugins to provide custom exception mappings for the REST API. * Added API exception registry that allows plugins to provide custom exception mappings for the REST API.
* Added formula round and int functions. [#891](https://gitlab.com/bramw/baserow/-/issues/891) * Added formula round and int functions. [#891](https://gitlab.com/bramw/baserow/-/issues/891)
* Link to table field can now link rows in the same table. [#798](https://gitlab.com/bramw/baserow/-/issues/798)
### Bug Fixes ### Bug Fixes
@ -42,6 +43,7 @@ For example:
### Breaking Changes ### Breaking Changes
## Released (2022-06-09 1.10.1) ## Released (2022-06-09 1.10.1)
* Plugins can now include their own menu or other template in the main menu sidebar. * Plugins can now include their own menu or other template in the main menu sidebar.

View file

@ -65,7 +65,7 @@ export default {
for (let tableI = 0; tableI < application.tables.length; tableI++) { for (let tableI = 0; tableI < application.tables.length; tableI++) {
const table = application.tables[tableI] const table = application.tables[tableI]
if (table.id === tableId) { if (table.id === tableId) {
return application.tables.filter((t) => t.id !== tableId) return application.tables
} }
} }
} }