1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-22 20:32:24 +00:00

Merge branch 'fix_baserow_table_data_sync_import_export_bug' into 'develop'

Fix Baserow table data sync export import authorized user and source table bug

See merge request 
This commit is contained in:
Bram Wiepjes 2024-11-05 14:37:59 +00:00
commit 8cd1bcc45a
12 changed files with 353 additions and 10 deletions
backend/src/baserow
changelog/entries/unreleased/bug
enterprise/backend
src/baserow_enterprise/data_sync
tests/baserow_enterprise_tests/data_sync
web-frontend/modules
builder/components/page
database/components/sidebar

View file

@ -533,7 +533,7 @@ class DatabaseApplicationType(ApplicationType):
# metadata is imported too. # metadata is imported too.
self._import_extra_metadata(serialized_tables, id_mapping, import_export_config) self._import_extra_metadata(serialized_tables, id_mapping, import_export_config)
self._import_data_sync(serialized_tables, id_mapping) self._import_data_sync(serialized_tables, id_mapping, import_export_config)
return imported_tables return imported_tables
@ -552,14 +552,16 @@ class DatabaseApplicationType(ApplicationType):
source_workspace, table, serialized_table, import_export_config source_workspace, table, serialized_table, import_export_config
) )
def _import_data_sync(self, serialized_tables, id_mapping): def _import_data_sync(self, serialized_tables, id_mapping, import_export_config):
for serialized_table in serialized_tables: for serialized_table in serialized_tables:
if not serialized_table.get("data_sync", None): if not serialized_table.get("data_sync", None):
continue continue
table = serialized_table["_object"] table = serialized_table["_object"]
serialized_data_sync = serialized_table["data_sync"] serialized_data_sync = serialized_table["data_sync"]
data_sync_type = data_sync_type_registry.get(serialized_data_sync["type"]) data_sync_type = data_sync_type_registry.get(serialized_data_sync["type"])
data_sync_type.import_serialized(table, serialized_data_sync, id_mapping) data_sync_type.import_serialized(
table, serialized_data_sync, id_mapping, import_export_config
)
def _import_table_rows( def _import_table_rows(
self, self,

View file

@ -246,6 +246,7 @@ class DataSyncHandler:
progress = ChildProgressBuilder.build(progress_builder, 100) progress = ChildProgressBuilder.build(progress_builder, 100)
data_sync_type = data_sync_type_registry.get_by_model(data_sync) data_sync_type = data_sync_type_registry.get_by_model(data_sync)
data_sync_type.before_sync_table(user, data_sync)
all_properties = data_sync_type.get_properties(data_sync) all_properties = data_sync_type.get_properties(data_sync)
key_to_property = {p.key: p for p in all_properties} key_to_property = {p.key: p for p in all_properties}
progress.increment(by=1) progress.increment(by=1)

View file

@ -9,6 +9,7 @@ from baserow.contrib.database.data_sync.export_serialized import (
) )
from baserow.contrib.database.data_sync.models import DataSync, DataSyncSyncedProperty from baserow.contrib.database.data_sync.models import DataSync, DataSyncSyncedProperty
from baserow.contrib.database.fields.models import Field from baserow.contrib.database.fields.models import Field
from baserow.core.registries import ImportExportConfig
from baserow.core.registry import ( from baserow.core.registry import (
CustomFieldsInstanceMixin, CustomFieldsInstanceMixin,
CustomFieldsRegistryMixin, CustomFieldsRegistryMixin,
@ -94,6 +95,14 @@ class DataSyncType(
:param instance: The related data sync instance. :param instance: The related data sync instance.
""" """
def before_sync_table(self, user: AbstractUser, instance: "DataSync"):
"""
A hook that's called right before the table sync starts.
:param user: The user on whose behalf the table is synced.
:param instance: The related data sync instance.
"""
@abstractmethod @abstractmethod
def get_properties(self, instance: "DataSync") -> List[DataSyncProperty]: def get_properties(self, instance: "DataSync") -> List[DataSyncProperty]:
""" """
@ -155,7 +164,13 @@ class DataSyncType(
**type_specific, **type_specific,
) )
def import_serialized(self, table, serialized_values, id_mapping): def import_serialized(
self,
table,
serialized_values,
id_mapping,
import_export_config: ImportExportConfig,
):
""" """
Imports the data sync properties and the `allowed_fields`. Imports the data sync properties and the `allowed_fields`.
""" """

View file

@ -835,7 +835,9 @@ class TableHandler(metaclass=baserow_trace_methods(tracer)):
database_type = application_type_registry.get_by_model(database) database_type = application_type_registry.get_by_model(database)
config = ImportExportConfig( config = ImportExportConfig(
include_permission_data=True, reduce_disk_space_usage=False include_permission_data=True,
reduce_disk_space_usage=False,
is_duplicate=True,
) )
serialized_tables = database_type.export_tables_serialized([table], config) serialized_tables = database_type.export_tables_serialized([table], config)

View file

@ -1494,7 +1494,9 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
progress.increment(by=start_progress) progress.increment(by=start_progress)
duplicate_import_export_config = ImportExportConfig( duplicate_import_export_config = ImportExportConfig(
include_permission_data=True, reduce_disk_space_usage=False include_permission_data=True,
reduce_disk_space_usage=False,
is_duplicate=True,
) )
# export the application # export the application
specific_application = application.specific specific_application = application.specific

View file

@ -70,6 +70,7 @@ class ImportExportConfig:
include_permission_data: bool include_permission_data: bool
reduce_disk_space_usage: bool = False
""" """
Whether or not the import/export should attempt to save disk space by excluding Whether or not the import/export should attempt to save disk space by excluding
certain pieces of optional data or processes that could instead be done later or certain pieces of optional data or processes that could instead be done later or
@ -79,13 +80,18 @@ class ImportExportConfig:
tsvector full text search columns as they can also be lazy loaded after the import tsvector full text search columns as they can also be lazy loaded after the import
when the user opens a view. when the user opens a view.
""" """
reduce_disk_space_usage: bool = False
workspace_for_user_references: "Workspace" = None
""" """
Determines an alternative workspace to search for user references Determines an alternative workspace to search for user references
during imports. during imports.
""" """
workspace_for_user_references: "Workspace" = None
is_duplicate: bool = False
"""
Indicates whether the import export operation is duplicating an existing object.
The data then doesn't leave the instance.
"""
class Plugin(APIUrlsInstanceMixin, Instance): class Plugin(APIUrlsInstanceMixin, Instance):

View file

@ -394,6 +394,7 @@ class SnapshotHandler:
include_permission_data=True, include_permission_data=True,
reduce_disk_space_usage=True, reduce_disk_space_usage=True,
workspace_for_user_references=workspace, workspace_for_user_references=workspace,
is_duplicate=True,
) )
try: try:
exported_application = application_type.export_serialized( exported_application = application_type.export_serialized(
@ -454,7 +455,9 @@ class SnapshotHandler:
application_type = application_type_registry.get_by_model(application) application_type = application_type_registry.get_by_model(application)
restore_snapshot_import_export_config = ImportExportConfig( restore_snapshot_import_export_config = ImportExportConfig(
include_permission_data=True, reduce_disk_space_usage=False include_permission_data=True,
reduce_disk_space_usage=False,
is_duplicate=True,
) )
# Temporary set the workspace for the application so that the permissions can # Temporary set the workspace for the application so that the permissions can
# be correctly set during the import process. # be correctly set during the import process.

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "Fix Baserow table data sync export import authorized user and source table bug.",
"issue_number": null,
"bullet_points": [],
"created_at": "2024-10-24"
}

View file

@ -35,6 +35,7 @@ from baserow.contrib.database.fields.models import (
TextField, TextField,
) )
from baserow.contrib.database.fields.registries import field_type_registry from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.fields.utils import get_field_id_from_field_key
from baserow.contrib.database.rows.operations import ReadDatabaseRowOperationType from baserow.contrib.database.rows.operations import ReadDatabaseRowOperationType
from baserow.contrib.database.table.exceptions import TableDoesNotExist from baserow.contrib.database.table.exceptions import TableDoesNotExist
from baserow.contrib.database.table.handler import TableHandler from baserow.contrib.database.table.handler import TableHandler
@ -144,6 +145,15 @@ class LocalBaserowTableDataSyncType(DataSyncType):
DATA_SYNC, instance.table.database.workspace DATA_SYNC, instance.table.database.workspace
) )
def before_sync_table(self, user, instance):
# If the authorized user was deleted, or the table was duplicated,
# the authorized user is set to `None`. In this case, we're setting the
# authorized user to the user on whos behalf the table is synced so that it
# will work.
if instance.authorized_user is None:
instance.authorized_user = user
instance.save()
def _get_table(self, instance): def _get_table(self, instance):
try: try:
table = TableHandler().get_table(instance.source_table_id) table = TableHandler().get_table(instance.source_table_id)
@ -201,3 +211,54 @@ class LocalBaserowTableDataSyncType(DataSyncType):
rows_queryset = model.objects.all().values(*["id"] + enabled_property_field_ids) rows_queryset = model.objects.all().values(*["id"] + enabled_property_field_ids)
progress.increment(by=9) # makes the total `10` progress.increment(by=9) # makes the total `10`
return rows_queryset return rows_queryset
def import_serialized(
self, table, serialized_values, id_mapping, import_export_config
):
serialized_copy = serialized_values.copy()
# Always unset the authorized user for security reasons. This is okay because
# the first user to sync the data sync table will become the authorized user.
serialized_copy["authorized_user_id"] = None
source_table_id = serialized_copy["source_table_id"]
if source_table_id in id_mapping["database_tables"]:
# If the source table exists in the mapping, it means that it was
# included in the export. In that case, we want to use that one as source
# table instead of the existing one.
serialized_copy["source_table_id"] = id_mapping["database_tables"][
source_table_id
]
serialized_copy["authorized_user_id"] = None
data_sync = super().import_serialized(
table, serialized_copy, id_mapping, import_export_config
)
# Because we're now pointing to the newly imported data sync source table,
# the field id keys must also be remapped.
properties_to_update = []
for data_sync_property in data_sync.synced_properties.all():
key_field_id = get_field_id_from_field_key(data_sync_property.key)
if key_field_id:
new_field_id = id_mapping["database_fields"][key_field_id]
data_sync_property.key = f"field_{new_field_id}"
properties_to_update.append(data_sync_property)
DataSyncSyncedProperty.objects.bulk_update(properties_to_update, ["key"])
return data_sync
if import_export_config.is_duplicate:
# When duplicating the database or table, and it doesn't exist in the
# id_mapping, then the source table is inside the same database or in
# another workspace. In that case, we want to keep using the same.
return super().import_serialized(
table, serialized_copy, id_mapping, import_export_config
)
# If the source table doesn't exist in the mapping, and we're not
# duplicating, then it's not possible to preserve the data sync. We'll then
# transform the fields to editable fields, keep the data, and keep the table
# as regular table.
table.field_set.all().update(
read_only=False, immutable_type=False, immutable_properties=False
)
return None

View file

@ -1,3 +1,4 @@
from django.core.exceptions import ObjectDoesNotExist
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
@ -14,7 +15,9 @@ from baserow.contrib.database.data_sync.exceptions import SyncError
from baserow.contrib.database.data_sync.handler import DataSyncHandler from baserow.contrib.database.data_sync.handler import DataSyncHandler
from baserow.contrib.database.fields.models import NumberField from baserow.contrib.database.fields.models import NumberField
from baserow.contrib.database.fields.registries import field_type_registry from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.table.handler import TableHandler
from baserow.core.db import specific_iterator from baserow.core.db import specific_iterator
from baserow.core.registries import ImportExportConfig, application_type_registry
from baserow.test_utils.helpers import setup_interesting_test_table from baserow.test_utils.helpers import setup_interesting_test_table
from baserow_enterprise.data_sync.baserow_table_data_sync import ( from baserow_enterprise.data_sync.baserow_table_data_sync import (
BaserowFieldDataSyncProperty, BaserowFieldDataSyncProperty,
@ -235,6 +238,70 @@ def test_sync_data_sync_table(enterprise_data_fixture):
) )
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_sync_data_sync_table_authorized_user_is_none(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user = enterprise_data_fixture.create_user()
source_table = enterprise_data_fixture.create_database_table(
user=user, name="Source"
)
database = enterprise_data_fixture.create_database_application(user=user)
handler = DataSyncHandler()
data_sync = handler.create_data_sync_table(
user=user,
database=database,
table_name="Test",
type_name="local_baserow_table",
synced_properties=["id"],
source_table_id=source_table.id,
authorized_user=None,
)
handler.sync_data_sync_table(user=user, data_sync=data_sync)
data_sync.refresh_from_db()
assert data_sync.authorized_user_id == user.id
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_sync_data_sync_table_authorized_user_is_set(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user = enterprise_data_fixture.create_user()
user_2 = enterprise_data_fixture.create_user()
workspace = enterprise_data_fixture.create_workspace(user=user)
enterprise_data_fixture.create_user_workspace(
workspace=workspace, user=user_2, order=0
)
database = enterprise_data_fixture.create_database_application(workspace=workspace)
source_table = enterprise_data_fixture.create_database_table(
database=database, name="Source"
)
handler = DataSyncHandler()
data_sync = handler.create_data_sync_table(
user=user,
database=database,
table_name="Test",
type_name="local_baserow_table",
synced_properties=["id"],
source_table_id=source_table.id,
authorized_user=user,
)
handler.sync_data_sync_table(user=user_2, data_sync=data_sync)
data_sync.refresh_from_db()
assert data_sync.authorized_user_id == user.id
@pytest.mark.django_db @pytest.mark.django_db
@override_settings(DEBUG=True) @override_settings(DEBUG=True)
def test_sync_data_sync_table_with_interesting_table_as_source(enterprise_data_fixture): def test_sync_data_sync_table_with_interesting_table_as_source(enterprise_data_fixture):
@ -639,3 +706,177 @@ def test_async_sync_data_sync_table_without_license(
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
assert response.status_code == HTTP_402_PAYMENT_REQUIRED assert response.status_code == HTTP_402_PAYMENT_REQUIRED
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_import_export_including_source_table(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user = enterprise_data_fixture.create_user()
workspace = enterprise_data_fixture.create_workspace(user=user)
database = enterprise_data_fixture.create_database_application(workspace=workspace)
source_table = enterprise_data_fixture.create_database_table(
name="Source", database=database
)
text_field = enterprise_data_fixture.create_text_field(
table=source_table, name="Text"
)
handler = DataSyncHandler()
data_sync = handler.create_data_sync_table(
user=user,
database=database,
table_name="Test",
type_name="local_baserow_table",
synced_properties=["id", f"field_{text_field.id}"],
source_table_id=source_table.id,
)
properties = data_sync.synced_properties.all().order_by("key")
handler.sync_data_sync_table(user=user, data_sync=data_sync)
database_type = application_type_registry.get("database")
config = ImportExportConfig(include_permission_data=True)
serialized = database_type.export_serialized(database, config)
imported_workspace = enterprise_data_fixture.create_workspace()
imported_workspace_user = enterprise_data_fixture.create_user_workspace(
workspace=imported_workspace, user=user
)
id_mapping = {}
imported_database = database_type.import_serialized(
imported_workspace,
serialized,
config,
id_mapping,
None,
None,
)
imported_table = imported_database.table_set.filter(name="Test").first()
imported_source_table = imported_database.table_set.filter(name="Source").first()
imported_data_sync = imported_table.data_sync.specific
imported_text_field = imported_source_table.field_set.all().first()
assert imported_data_sync.authorized_user_id is None
assert imported_data_sync.source_table_id == imported_source_table.id
fields = imported_data_sync.table.field_set.all().order_by("id")
assert fields[0].read_only is True
assert fields[1].read_only is True
imported_properties = imported_data_sync.synced_properties.all().order_by("key")
assert imported_properties[0].key != f"field_{text_field.id}"
assert imported_properties[0].key == f"field_{imported_text_field.id}"
assert imported_properties[1].key == "id"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_import_export_duplicate_table(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user = enterprise_data_fixture.create_user()
workspace = enterprise_data_fixture.create_workspace(user=user)
database = enterprise_data_fixture.create_database_application(workspace=workspace)
source_table = enterprise_data_fixture.create_database_table(
name="Source", database=database
)
text_field = enterprise_data_fixture.create_text_field(
table=source_table, name="Text"
)
handler = DataSyncHandler()
data_sync = handler.create_data_sync_table(
user=user,
database=database,
table_name="Test",
type_name="local_baserow_table",
synced_properties=["id", f"field_{text_field.id}"],
source_table_id=source_table.id,
)
handler.sync_data_sync_table(user=user, data_sync=data_sync)
duplicated_table = TableHandler().duplicate_table(user, data_sync.table)
assert duplicated_table.id != data_sync.table_id
imported_data_sync = duplicated_table.data_sync.specific
assert imported_data_sync.source_table_id == data_sync.source_table_id
assert imported_data_sync.authorized_user_id is None
assert imported_data_sync.authorized_user_id is None
fields = imported_data_sync.table.field_set.all().order_by("id")
assert fields[0].read_only is True
assert fields[1].read_only is True
imported_properties = imported_data_sync.synced_properties.all().order_by("key")
assert imported_properties[0].key == f"field_{text_field.id}"
assert imported_properties[1].key == "id"
@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_import_export_excluding_source_table(enterprise_data_fixture):
enterprise_data_fixture.enable_enterprise()
user = enterprise_data_fixture.create_user()
workspace = enterprise_data_fixture.create_workspace(user=user)
workspace_2 = enterprise_data_fixture.create_workspace(user=user)
database = enterprise_data_fixture.create_database_application(workspace=workspace)
database_2 = enterprise_data_fixture.create_database_application(
workspace=workspace_2
)
source_table = enterprise_data_fixture.create_database_table(
name="Source", database=database_2
)
text_field = enterprise_data_fixture.create_text_field(
table=source_table, name="Text"
)
handler = DataSyncHandler()
data_sync = handler.create_data_sync_table(
user=user,
database=database,
table_name="Test",
type_name="local_baserow_table",
synced_properties=["id", f"field_{text_field.id}"],
source_table_id=source_table.id,
)
properties = data_sync.synced_properties.all().order_by("key")
handler.sync_data_sync_table(user=user, data_sync=data_sync)
database_type = application_type_registry.get("database")
config = ImportExportConfig(include_permission_data=True)
serialized = database_type.export_serialized(database, config)
imported_workspace = enterprise_data_fixture.create_workspace()
imported_workspace_user = enterprise_data_fixture.create_user_workspace(
workspace=imported_workspace, user=user
)
id_mapping = {}
imported_database = database_type.import_serialized(
imported_workspace,
serialized,
config,
id_mapping,
None,
None,
)
imported_table = imported_database.table_set.filter(name="Test").first()
with pytest.raises(ObjectDoesNotExist):
imported_table.data_sync
fields = imported_table.field_set.all().order_by("id")
assert fields[0].read_only is False
assert fields[0].immutable_properties is False
assert fields[0].immutable_type is False
assert fields[1].read_only is False
assert fields[1].immutable_properties is False
assert fields[1].immutable_type is False

View file

@ -2,7 +2,6 @@
<li <li
class="tree__item" class="tree__item"
:class="{ :class="{
active: application._.selected,
'tree__item--loading': application._.loading, 'tree__item--loading': application._.loading,
}" }"
> >

View file

@ -20,6 +20,10 @@
:class="{ active: isTableActive(table) }" :class="{ active: isTableActive(table) }"
> >
<a class="tree__sub-link" @click="selectTable(application, table)"> <a class="tree__sub-link" @click="selectTable(application, table)">
<i
v-if="table.data_sync"
class="context__menu-item-icon iconoir-data-transfer-down"
></i>
{{ table.name }} {{ table.name }}
</a> </a>
</li> </li>