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:
parent
5143374025
commit
e14a4205d0
20 changed files with 748 additions and 82 deletions
backend
mypy.ini
changelog.mdsrc/baserow/contrib/database
tests/baserow/contrib/database
web-frontend/modules/database/components/field
|
@ -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"
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue