1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 22:35:36 +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", {})
foreign_table_id = type_options.get("foreignTableId")
# Only return a link row field if the foreign table id is not the same as the
# current table id because we're not supported link_row fields that point to
# the same table.
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"),
)
return LinkRowField(
link_row_table_id=foreign_table_id,
link_row_related_field_id=type_options.get("symmetricColumnId"),
)
def to_baserow_export_serialized_value(
self,

View file

@ -6,7 +6,10 @@ from django.urls import path, include
from django.utils import timezone
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 (
FieldUpdateCollector,
)
@ -225,7 +228,7 @@ class DatabaseApplicationType(ApplicationType):
add_dependencies=False,
)
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
# because the export contains correct values and we don't want them

View file

@ -260,6 +260,38 @@ def optional_atomic(atomic=True):
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
def safe_django_schema_editor(atomic=True, **kwargs):
# 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_overrides = {
"link_row_related_field": serializers.PrimaryKeyRelatedField(read_only=True)
"link_row_related_field": serializers.PrimaryKeyRelatedField(
read_only=True, required=False
)
}
api_exceptions_map = {
LinkRowTableNotProvided: ERROR_LINK_ROW_TABLE_NOT_PROVIDED,
@ -1189,13 +1191,16 @@ class LinkRowFieldType(FieldType):
manytomany_models[instance.table_id] = model
# Check if the related table model is already in the manytomany_models.
related_model = manytomany_models.get(instance.link_row_table_id)
# If we do not have a related table model already we can generate a new one.
if not related_model:
related_model = instance.link_row_table.get_model(
manytomany_models=manytomany_models
)
is_referencing_the_same_table = instance.link_row_table_id == instance.table_id
if is_referencing_the_same_table:
related_model = model
else:
related_model = manytomany_models.get(instance.link_row_table_id)
# 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
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
# the related name should be. If the related if is not found that means that it
# has not yet been created.
for related_field in related_model._field_objects.values():
if (
def field_is_link_row_related_field(related_field):
return (
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 == instance.id
):
related_name = related_field["name"]
)
# Note that the through model will not be registered with the apps because of
# the `DatabaseConfig.prevent_generated_model_for_registering` hack.
if not is_referencing_the_same_table:
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(
to=related_model,
related_name=related_name,
@ -1222,16 +1232,30 @@ class LinkRowFieldType(FieldType):
db_constraint=False,
).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
# created ManyToManyField. They need to be called manually because normally
# 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
# register new pending operations.
apps = model._meta.apps
model_field = model._meta.get_field(field_name)
apps.do_pending_operations(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()
def prepare_values(self, values, user):
@ -1309,7 +1333,7 @@ class LinkRowFieldType(FieldType):
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
related_field_name = self.find_next_unused_related_field_name(field)
@ -1347,7 +1371,10 @@ class LinkRowFieldType(FieldType):
to_model_field,
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
# related field can be deleted.
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
# related 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
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()
if from_field.link_row_related_field is None:
# we need to create the missing link_row_related_field
to_field.link_row_related_field = FieldHandler().create_field(
user=user,
table=to_field.link_row_table,
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(
self,
@ -1383,8 +1432,10 @@ class LinkRowFieldType(FieldType):
field into the related table.
"""
if not isinstance(from_field, self.model_class) and isinstance(
to_field, self.model_class
if (
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)
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.
"""
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):
"""
@ -1510,8 +1562,11 @@ class LinkRowFieldType(FieldType):
):
getattr(row, field_name).set(value)
def get_other_fields_to_trash_restore_always_together(self, field) -> List[Any]:
return [field.link_row_related_field]
def get_other_fields_to_trash_restore_always_together(self, field) -> List[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:
primary_field = field.get_related_primary_field()

View file

@ -147,6 +147,8 @@ class FieldDependencyExtractingVisitor(
if primary_field_in_other_table is None:
return []
else:
if primary_field_in_other_table.id == source_field.id:
raise SelfReferenceFieldDependencyError()
return [
FieldDependency(
dependant=source_field,
@ -171,6 +173,8 @@ class FieldDependencyExtractingVisitor(
),
]
else:
if target_field.id == source_field.id:
raise SelfReferenceFieldDependencyError()
# 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.
return [

View file

@ -743,13 +743,24 @@ class RowHandler:
value_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
# value and which on contains the value.
for field in through_fields:
if type(field) is not ForeignKey:
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]
else:
value_column = field.get_attname_column()[1]
@ -939,6 +950,11 @@ class RowHandler:
value_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
# value and which one contains the value.
for field in through_fields:
@ -947,7 +963,14 @@ class RowHandler:
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_name = row_column
else:

View file

@ -350,12 +350,11 @@ class RowsTrashableItemType(TrashableItemType):
def restore(self, trashed_item, trash_entry: TrashEntry):
table = self._get_table(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
).enhance_by_fields()
for row in rows_to_restore:
row.trashed = False
table_model.objects_and_trash.bulk_update(rows_to_restore, ["trashed"])
)
rows_to_restore_queryset.update(trashed=False)
rows_to_restore = rows_to_restore_queryset.enhance_by_fields()
trashed_item.delete()
field_cache = FieldCache()

View file

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

View file

@ -665,6 +665,7 @@ def test_airtable_import_foreign_key_column(data_fixture, api_client):
== [1, 2]
)
# link to same table row
airtable_field = {
"id": "fldQcEaGEe7xuhUEuPL",
"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(
{"id": "tblRpq315qnnIcg5IjI"}, airtable_field, UTC
)
assert baserow_field is None
assert airtable_column_type is None
assert isinstance(baserow_field, LinkRowField)
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

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]["name"] == "Data"
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
# 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
@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.field_link_row
@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_2, f"field_{link_field.id}").count() == 2
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.field_link_row
def test_dependencies_for_link_row_link_row_self_reference(data_fixture):
user = data_fixture.create_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",
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(
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.field_link_row
def test_get_dependant_fields_with_type(data_fixture):
table = data_fixture.create_database_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():
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")
model = table.get_model()

View file

@ -1,25 +1,27 @@
import pytest
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 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 (
LinkRowTableNotInSameDatabase,
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.core.handler import CoreHandler
from baserow.core.trash.handler import TrashHandler
@pytest.mark.django_db
@pytest.mark.field_link_row
def test_call_apps_registry_pending_operations(data_fixture):
user = data_fixture.create_user()
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.field_link_row
def test_link_row_field_type(data_fixture):
user = data_fixture.create_user()
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.field_link_row
def test_link_row_field_type_rows(data_fixture):
user = data_fixture.create_user()
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.field_link_row
def test_link_row_enhance_queryset(data_fixture, django_assert_num_queries):
user = data_fixture.create_user()
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.field_link_row
def test_link_row_field_type_api_views(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
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.field_link_row
def test_link_row_field_type_api_row_views(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
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.field_link_row
def test_import_export_link_row_field(data_fixture):
user = data_fixture.create_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.field_link_row
def test_creating_a_linked_row_pointing_at_trashed_row_works_but_does_not_display(
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.field_link_row
def test_change_type_to_link_row_field_when_field_with_same_related_name_already_exists(
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}": "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(
user=user,
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.field_link_row
def test_change_link_row_related_table_when_field_with_related_name_exists(
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"
)
# 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(
user=user,
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 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,
)
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)
select_option_1 = SelectOption.objects.create(
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_{fuel_type_option_field.id}": option_gasoline.id,
manufacturer_link_row_field.id: [alfa_manufacturer.id],
alternative_car_link_row_field.id: [car.id],
year_of_manifacture.id: "2015-09-01",
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}"
).values_list("id", flat=True)
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"
options = getattr(car, f"field_{multiple_select_field.id}").values_list(
"id", flat=True
@ -788,6 +801,10 @@ def test_can_undo_redo_updating_row(data_fixture):
car, f"field_{manufacturer_link_row_field.id}"
).values_list("id", flat=True)
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 not getattr(car, "field_9999", None)
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}"
).values_list("id", flat=True)
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"
options = getattr(car, f"field_{multiple_select_field.id}").values_list(
"id", flat=True

View file

@ -19,6 +19,7 @@ def test_import_export_database(data_fixture):
formula=f"field('{text_field.name}')",
formula_type="text",
)
data_fixture.create_link_row_field(table=table, link_row_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_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.name == table.name
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
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
@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
def test_link_row_has_not_filter_type(data_fixture):
user = data_fixture.create_user()

View file

@ -20,6 +20,7 @@ For example:
* 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 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
@ -42,6 +43,7 @@ For example:
### Breaking Changes
## Released (2022-06-09 1.10.1)
* 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++) {
const table = application.tables[tableI]
if (table.id === tableId) {
return application.tables.filter((t) => t.id !== tableId)
return application.tables
}
}
}