mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-28 06:22:24 +00:00
Merge branch 'field-airtable-import-improvements' into 'develop'
Airtable import field type improvements Closes #3455 See merge request baserow/baserow!3159
This commit is contained in:
commit
5412427ca8
13 changed files with 1444 additions and 119 deletions
backend
src/baserow/contrib/database
tests
airtable_responses/basic
baserow/contrib/database/airtable
changelog/entries/unreleased/feature
|
@ -1,16 +1,18 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
|
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
|
||||||
from baserow.contrib.database.fields.models import (
|
from baserow.contrib.database.fields.models import (
|
||||||
NUMBER_MAX_DECIMAL_PLACES,
|
NUMBER_MAX_DECIMAL_PLACES,
|
||||||
|
AutonumberField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
CountField,
|
CountField,
|
||||||
CreatedOnField,
|
CreatedOnField,
|
||||||
DateField,
|
DateField,
|
||||||
|
DurationField,
|
||||||
EmailField,
|
EmailField,
|
||||||
Field,
|
Field,
|
||||||
FileField,
|
FileField,
|
||||||
|
@ -26,9 +28,18 @@ from baserow.contrib.database.fields.models import (
|
||||||
URLField,
|
URLField,
|
||||||
)
|
)
|
||||||
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.duration import D_H, H_M_S_SSS
|
||||||
|
from baserow.core.utils import get_value_at_path
|
||||||
|
|
||||||
from .config import AirtableImportConfig
|
from .config import AirtableImportConfig
|
||||||
from .constants import AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING
|
from .constants import (
|
||||||
|
AIRTABLE_DURATION_FIELD_DURATION_FORMAT_MAPPING,
|
||||||
|
AIRTABLE_MAX_DURATION_VALUE,
|
||||||
|
AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING,
|
||||||
|
AIRTABLE_RATING_COLOR_MAPPING,
|
||||||
|
AIRTABLE_RATING_ICON_MAPPING,
|
||||||
|
)
|
||||||
|
from .exceptions import AirtableSkipCellValue
|
||||||
from .helpers import import_airtable_date_type_options, set_select_options_on_field
|
from .helpers import import_airtable_date_type_options, set_select_options_on_field
|
||||||
from .import_report import (
|
from .import_report import (
|
||||||
ERROR_TYPE_DATA_TYPE_MISMATCH,
|
ERROR_TYPE_DATA_TYPE_MISMATCH,
|
||||||
|
@ -38,7 +49,7 @@ from .import_report import (
|
||||||
AirtableImportReport,
|
AirtableImportReport,
|
||||||
)
|
)
|
||||||
from .registry import AirtableColumnType
|
from .registry import AirtableColumnType
|
||||||
from .utils import get_airtable_row_primary_value
|
from .utils import get_airtable_row_primary_value, quill_to_markdown
|
||||||
|
|
||||||
|
|
||||||
class TextAirtableColumnType(AirtableColumnType):
|
class TextAirtableColumnType(AirtableColumnType):
|
||||||
|
@ -53,7 +64,7 @@ class TextAirtableColumnType(AirtableColumnType):
|
||||||
elif validator_name == "email":
|
elif validator_name == "email":
|
||||||
return EmailField()
|
return EmailField()
|
||||||
else:
|
else:
|
||||||
return TextField()
|
return TextField(text_default=raw_airtable_column.get("default", ""))
|
||||||
|
|
||||||
def to_baserow_export_serialized_value(
|
def to_baserow_export_serialized_value(
|
||||||
self,
|
self,
|
||||||
|
@ -86,6 +97,25 @@ class TextAirtableColumnType(AirtableColumnType):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def to_baserow_export_empty_value(
|
||||||
|
self,
|
||||||
|
row_id_mapping,
|
||||||
|
raw_airtable_table,
|
||||||
|
raw_airtable_row,
|
||||||
|
raw_airtable_column,
|
||||||
|
baserow_field,
|
||||||
|
files_to_download,
|
||||||
|
config,
|
||||||
|
import_report,
|
||||||
|
):
|
||||||
|
# If the `text_default` is set, then we must return an empty string. If we
|
||||||
|
# don't, the value is omitted in the export, resulting in the default value
|
||||||
|
# automatically being set, while it's actually empty in Airtable.
|
||||||
|
if isinstance(baserow_field, TextField) and baserow_field.text_default != "":
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
raise AirtableSkipCellValue
|
||||||
|
|
||||||
|
|
||||||
class MultilineTextAirtableColumnType(AirtableColumnType):
|
class MultilineTextAirtableColumnType(AirtableColumnType):
|
||||||
type = "multilineText"
|
type = "multilineText"
|
||||||
|
@ -102,7 +132,7 @@ class RichTextTextAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
return LongTextField()
|
return LongTextField(long_text_enable_rich_text=True)
|
||||||
|
|
||||||
def to_baserow_export_serialized_value(
|
def to_baserow_export_serialized_value(
|
||||||
self,
|
self,
|
||||||
|
@ -116,37 +146,7 @@ class RichTextTextAirtableColumnType(AirtableColumnType):
|
||||||
config,
|
config,
|
||||||
import_report,
|
import_report,
|
||||||
):
|
):
|
||||||
# We don't support rich text formatting yet, so this converts the value to
|
return quill_to_markdown(value["documentValue"])
|
||||||
# plain text.
|
|
||||||
rich_values = []
|
|
||||||
for v in value["documentValue"]:
|
|
||||||
insert_value = v["insert"]
|
|
||||||
if isinstance(insert_value, str):
|
|
||||||
rich_values.append(insert_value)
|
|
||||||
elif isinstance(insert_value, dict):
|
|
||||||
rich_value = self._extract_value_from_airtable_rich_value_dict(
|
|
||||||
insert_value
|
|
||||||
)
|
|
||||||
if rich_value is not None:
|
|
||||||
rich_values.append(rich_value)
|
|
||||||
|
|
||||||
return "".join(rich_values)
|
|
||||||
|
|
||||||
def _extract_value_from_airtable_rich_value_dict(
|
|
||||||
self, insert_value_dict: Dict[Any, Any]
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Airtable rich text fields can contain references to users. For now this method
|
|
||||||
attempts to return a @userId reference string. In the future if Baserow has
|
|
||||||
a rich text field and the ability to reference users in them we should map
|
|
||||||
this airtable userId to the corresponding Baserow user id.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mention = insert_value_dict.get("mention")
|
|
||||||
if isinstance(mention, dict):
|
|
||||||
user_id = mention.get("userId")
|
|
||||||
if user_id is not None:
|
|
||||||
return f"@{user_id}"
|
|
||||||
|
|
||||||
|
|
||||||
class NumberAirtableColumnType(AirtableColumnType):
|
class NumberAirtableColumnType(AirtableColumnType):
|
||||||
|
@ -155,10 +155,51 @@ class NumberAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
|
self.add_import_report_failed_if_default_is_provided(
|
||||||
|
raw_airtable_table, raw_airtable_column, import_report
|
||||||
|
)
|
||||||
|
|
||||||
type_options = raw_airtable_column.get("typeOptions", {})
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
options_format = type_options.get("format", "")
|
options_format = type_options.get("format", "")
|
||||||
|
|
||||||
|
if options_format in ["duration", "durationInDays"]:
|
||||||
|
return self.to_duration_field(
|
||||||
|
raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.to_number_field(
|
||||||
|
raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_duration_field(
|
||||||
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
|
):
|
||||||
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
|
options_format = type_options.get("format", "")
|
||||||
|
duration_format = type_options.get("durationFormat", "")
|
||||||
|
|
||||||
|
if options_format == "durationInDays":
|
||||||
|
# It looks like this option is broken in Airtable. When this is selected,
|
||||||
|
# the exact value seems to be in seconds, but it should be in days. We
|
||||||
|
# will therefore convert it to days when calculating the value.
|
||||||
|
duration_format = D_H
|
||||||
|
else:
|
||||||
|
# Fallback to the most specific format because that leaves most of the
|
||||||
|
# value intact.
|
||||||
|
duration_format = AIRTABLE_DURATION_FIELD_DURATION_FORMAT_MAPPING.get(
|
||||||
|
duration_format, H_M_S_SSS
|
||||||
|
)
|
||||||
|
|
||||||
|
return DurationField(duration_format=duration_format)
|
||||||
|
|
||||||
|
def to_number_field(
|
||||||
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
|
):
|
||||||
suffix = ""
|
suffix = ""
|
||||||
|
|
||||||
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
|
options_format = type_options.get("format", "")
|
||||||
|
|
||||||
if "percent" in options_format:
|
if "percent" in options_format:
|
||||||
suffix = "%"
|
suffix = "%"
|
||||||
|
|
||||||
|
@ -173,7 +214,7 @@ class NumberAirtableColumnType(AirtableColumnType):
|
||||||
|
|
||||||
if separator_format != "" and number_separator == "":
|
if separator_format != "" and number_separator == "":
|
||||||
import_report.add_failed(
|
import_report.add_failed(
|
||||||
f"Number field: \"{raw_airtable_column['name']}\"",
|
raw_airtable_column["name"],
|
||||||
SCOPE_FIELD,
|
SCOPE_FIELD,
|
||||||
raw_airtable_table.get("name", ""),
|
raw_airtable_table.get("name", ""),
|
||||||
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
@ -204,14 +245,40 @@ class NumberAirtableColumnType(AirtableColumnType):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
|
options_format = type_options.get("format", "")
|
||||||
|
row_name = get_airtable_row_primary_value(raw_airtable_table, raw_airtable_row)
|
||||||
|
|
||||||
|
if options_format == "durationInDays":
|
||||||
|
# If the formatting is in days, we must multiply the raw value in seconds
|
||||||
|
# by the number of seconds in a day.
|
||||||
|
value = value * 60 * 60 * 24
|
||||||
|
|
||||||
|
if "duration" in options_format:
|
||||||
|
# If the value is higher than the maximum that the `timedelta` can handle,
|
||||||
|
# then we can't use it, so we have to drop it. The maximum number of days
|
||||||
|
# in `timedelta` is `999999999`, so the max number of seconds are
|
||||||
|
# 999999999 * 24 * 60 * 60 = 86399999913600.
|
||||||
|
if abs(value) > AIRTABLE_MAX_DURATION_VALUE:
|
||||||
|
import_report.add_failed(
|
||||||
|
f"Row: \"{row_name}\", field: \"{raw_airtable_column['name']}\"",
|
||||||
|
SCOPE_CELL,
|
||||||
|
raw_airtable_table["name"],
|
||||||
|
ERROR_TYPE_DATA_TYPE_MISMATCH,
|
||||||
|
f"Cell value was left empty because the duration seconds {value} "
|
||||||
|
f'is outside the -86399999913600 and 86399999913600 range."',
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If the value is a duration, then we can use the same value because both
|
||||||
|
# store it as seconds.
|
||||||
|
return value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = Decimal(value)
|
value = Decimal(value)
|
||||||
except InvalidOperation:
|
except InvalidOperation:
|
||||||
# If the value can't be parsed as decimal, then it might be corrupt, so we
|
# If the value can't be parsed as decimal, then it might be corrupt, so we
|
||||||
# need to inform the user and skip the import.
|
# need to inform the user and skip the import.
|
||||||
row_name = get_airtable_row_primary_value(
|
|
||||||
raw_airtable_table, raw_airtable_row
|
|
||||||
)
|
|
||||||
import_report.add_failed(
|
import_report.add_failed(
|
||||||
f"Row: \"{row_name}\", field: \"{raw_airtable_column['name']}\"",
|
f"Row: \"{row_name}\", field: \"{raw_airtable_column['name']}\"",
|
||||||
SCOPE_CELL,
|
SCOPE_CELL,
|
||||||
|
@ -224,8 +291,6 @@ class NumberAirtableColumnType(AirtableColumnType):
|
||||||
|
|
||||||
# Airtable stores 10% as 0.1, so we would need to multiply it by 100 so get the
|
# Airtable stores 10% as 0.1, so we would need to multiply it by 100 so get the
|
||||||
# correct value in Baserow.
|
# correct value in Baserow.
|
||||||
type_options = raw_airtable_column.get("typeOptions", {})
|
|
||||||
options_format = type_options.get("format", "")
|
|
||||||
if "percent" in options_format:
|
if "percent" in options_format:
|
||||||
value = value * 100
|
value = value * 100
|
||||||
|
|
||||||
|
@ -241,8 +306,39 @@ class RatingAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
|
airtable_icon = type_options.get("icon", "")
|
||||||
|
airtable_max = type_options.get("max", 5)
|
||||||
|
airtable_color = type_options.get("color", "")
|
||||||
|
|
||||||
|
style = AIRTABLE_RATING_ICON_MAPPING.get(airtable_icon, "")
|
||||||
|
if style == "":
|
||||||
|
style = list(AIRTABLE_RATING_ICON_MAPPING.values())[0]
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the icon {airtable_icon} does not "
|
||||||
|
f"exist, so it defaulted to {style}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
color = AIRTABLE_RATING_COLOR_MAPPING.get(airtable_color, "")
|
||||||
|
if color == "":
|
||||||
|
color = list(AIRTABLE_RATING_COLOR_MAPPING.values())[0]
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the color {airtable_color} does not "
|
||||||
|
f"exist, so it defaulted to {color}.",
|
||||||
|
)
|
||||||
|
|
||||||
return RatingField(
|
return RatingField(
|
||||||
max_value=raw_airtable_column.get("typeOptions", {}).get("max", 5)
|
max_value=airtable_max,
|
||||||
|
style=style,
|
||||||
|
color=color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,6 +348,32 @@ class CheckboxAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
|
self.add_import_report_failed_if_default_is_provided(
|
||||||
|
raw_airtable_table, raw_airtable_column, import_report
|
||||||
|
)
|
||||||
|
|
||||||
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
|
airtable_icon = type_options.get("icon", "check")
|
||||||
|
airtable_color = type_options.get("color", "green")
|
||||||
|
|
||||||
|
if airtable_icon != "check":
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the icon {airtable_icon} is not supported.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if airtable_color != "green":
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the color {airtable_color} is not supported.",
|
||||||
|
)
|
||||||
|
|
||||||
return BooleanField()
|
return BooleanField()
|
||||||
|
|
||||||
def to_baserow_export_serialized_value(
|
def to_baserow_export_serialized_value(
|
||||||
|
@ -275,6 +397,13 @@ class DateAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
|
self.add_import_report_failed_if_default_is_provided(
|
||||||
|
raw_airtable_table,
|
||||||
|
raw_airtable_column,
|
||||||
|
import_report,
|
||||||
|
to_human_readable_default=lambda x: "Current date",
|
||||||
|
)
|
||||||
|
|
||||||
type_options = raw_airtable_column.get("typeOptions", {})
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
# Check if a timezone is provided in the type options, if so, we might want
|
# Check if a timezone is provided in the type options, if so, we might want
|
||||||
# to use that timezone for the conversion later on.
|
# to use that timezone for the conversion later on.
|
||||||
|
@ -283,13 +412,6 @@ class DateAirtableColumnType(AirtableColumnType):
|
||||||
|
|
||||||
# date_force_timezone=None it the equivalent of airtable_timezone="client".
|
# date_force_timezone=None it the equivalent of airtable_timezone="client".
|
||||||
if airtable_timezone == "client":
|
if airtable_timezone == "client":
|
||||||
import_report.add_failed(
|
|
||||||
raw_airtable_column["name"],
|
|
||||||
SCOPE_FIELD,
|
|
||||||
raw_airtable_table.get("name", ""),
|
|
||||||
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
|
||||||
"The date field was imported, but the client timezone setting was dropped.",
|
|
||||||
)
|
|
||||||
airtable_timezone = None
|
airtable_timezone = None
|
||||||
|
|
||||||
return DateField(
|
return DateField(
|
||||||
|
@ -358,15 +480,6 @@ class FormulaAirtableColumnType(AirtableColumnType):
|
||||||
is_last_modified = display_type == "lastModifiedTime"
|
is_last_modified = display_type == "lastModifiedTime"
|
||||||
is_created = display_type == "createdTime"
|
is_created = display_type == "createdTime"
|
||||||
|
|
||||||
if is_last_modified or is_created and airtable_timezone == "client":
|
|
||||||
import_report.add_failed(
|
|
||||||
raw_airtable_column["name"],
|
|
||||||
SCOPE_FIELD,
|
|
||||||
raw_airtable_table.get("name", ""),
|
|
||||||
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
|
||||||
"The field was imported, but the client timezone setting was dropped.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# date_force_timezone=None it the equivalent of airtable_timezone="client".
|
# date_force_timezone=None it the equivalent of airtable_timezone="client".
|
||||||
if airtable_timezone == "client":
|
if airtable_timezone == "client":
|
||||||
airtable_timezone = None
|
airtable_timezone = None
|
||||||
|
@ -374,6 +487,22 @@ class FormulaAirtableColumnType(AirtableColumnType):
|
||||||
# The formula conversion isn't support yet, but because the Created on and
|
# The formula conversion isn't support yet, but because the Created on and
|
||||||
# Last modified fields work as a formula, we can convert those.
|
# Last modified fields work as a formula, we can convert those.
|
||||||
if is_last_modified:
|
if is_last_modified:
|
||||||
|
dependencies = type_options.get("dependencies", {})
|
||||||
|
all_column_modifications = dependencies.get(
|
||||||
|
"dependsOnAllColumnModifications", False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not all_column_modifications:
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the support to depend on "
|
||||||
|
f"specific fields was dropped because that's not supported by "
|
||||||
|
f"Baserow.",
|
||||||
|
)
|
||||||
|
|
||||||
return LastModifiedField(
|
return LastModifiedField(
|
||||||
date_show_tzinfo=date_show_tzinfo,
|
date_show_tzinfo=date_show_tzinfo,
|
||||||
date_force_timezone=airtable_timezone,
|
date_force_timezone=airtable_timezone,
|
||||||
|
@ -421,6 +550,54 @@ class ForeignKeyAirtableColumnType(AirtableColumnType):
|
||||||
):
|
):
|
||||||
type_options = raw_airtable_column.get("typeOptions", {})
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
foreign_table_id = type_options.get("foreignTableId")
|
foreign_table_id = type_options.get("foreignTableId")
|
||||||
|
relationship = type_options.get("relationship", "many") # can be: one
|
||||||
|
view_id_for_record_selection = type_options.get(
|
||||||
|
"viewIdForRecordSelection", None
|
||||||
|
)
|
||||||
|
filters_for_record_selection = type_options.get(
|
||||||
|
"filtersForRecordSelection", None
|
||||||
|
)
|
||||||
|
ai_matching_options = type_options.get("aiMatchingOptions", None)
|
||||||
|
|
||||||
|
if relationship != "many":
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but support for a one to many "
|
||||||
|
f"relationship was dropped because it's not supported by Baserow.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if view_id_for_record_selection is not None:
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but limiting record selection to a view "
|
||||||
|
f"was dropped because the views have not been imported.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters_for_record_selection is not None:
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but filtering record by a condition "
|
||||||
|
f"was dropped because it's not supported by Baserow.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if ai_matching_options is not None:
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but using AI to show top matches was "
|
||||||
|
f"dropped because it's not supported by Baserow.",
|
||||||
|
)
|
||||||
|
|
||||||
return LinkRowField(
|
return LinkRowField(
|
||||||
link_row_table_id=foreign_table_id,
|
link_row_table_id=foreign_table_id,
|
||||||
|
@ -531,12 +708,21 @@ class SelectAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
field = SingleSelectField()
|
id_value = raw_airtable_column.get("id", "")
|
||||||
field = set_select_options_on_field(
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
field,
|
|
||||||
raw_airtable_column.get("id", ""),
|
def get_default(x):
|
||||||
raw_airtable_column.get("typeOptions", {}),
|
return get_value_at_path(type_options, f"choices.{x}.name", "")
|
||||||
|
|
||||||
|
self.add_import_report_failed_if_default_is_provided(
|
||||||
|
raw_airtable_table,
|
||||||
|
raw_airtable_column,
|
||||||
|
import_report,
|
||||||
|
to_human_readable_default=get_default,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
field = SingleSelectField()
|
||||||
|
field = set_select_options_on_field(field, id_value, type_options)
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
@ -562,12 +748,27 @@ class MultiSelectAirtableColumnType(AirtableColumnType):
|
||||||
def to_baserow_field(
|
def to_baserow_field(
|
||||||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
):
|
):
|
||||||
field = MultipleSelectField()
|
id_value = raw_airtable_column.get("id", "")
|
||||||
field = set_select_options_on_field(
|
type_options = raw_airtable_column.get("typeOptions", {})
|
||||||
field,
|
|
||||||
raw_airtable_column.get("id", ""),
|
def get_default(default):
|
||||||
raw_airtable_column.get("typeOptions", {}),
|
default = default or []
|
||||||
|
return ", ".join(
|
||||||
|
[
|
||||||
|
get_value_at_path(type_options, f"choices.{v}.name", "")
|
||||||
|
for v in default
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_import_report_failed_if_default_is_provided(
|
||||||
|
raw_airtable_table,
|
||||||
|
raw_airtable_column,
|
||||||
|
import_report,
|
||||||
|
to_human_readable_default=get_default,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
field = MultipleSelectField()
|
||||||
|
field = set_select_options_on_field(field, id_value, type_options)
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
@ -631,3 +832,12 @@ class CountAirtableColumnType(AirtableColumnType):
|
||||||
import_report,
|
import_report,
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AutoNumberAirtableColumnType(AirtableColumnType):
|
||||||
|
type = "autoNumber"
|
||||||
|
|
||||||
|
def to_baserow_field(
|
||||||
|
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||||
|
):
|
||||||
|
return AutonumberField()
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
from baserow.contrib.database.fields.utils.duration import (
|
||||||
|
H_M,
|
||||||
|
H_M_S,
|
||||||
|
H_M_S_S,
|
||||||
|
H_M_S_SS,
|
||||||
|
H_M_S_SSS,
|
||||||
|
)
|
||||||
|
|
||||||
|
AIRTABLE_MAX_DURATION_VALUE = 86399999913600
|
||||||
AIRTABLE_EXPORT_JOB_DOWNLOADING_BASE = "downloading-base"
|
AIRTABLE_EXPORT_JOB_DOWNLOADING_BASE = "downloading-base"
|
||||||
AIRTABLE_EXPORT_JOB_CONVERTING = "converting"
|
AIRTABLE_EXPORT_JOB_CONVERTING = "converting"
|
||||||
AIRTABLE_EXPORT_JOB_DOWNLOADING_FILES = "downloading-files"
|
AIRTABLE_EXPORT_JOB_DOWNLOADING_FILES = "downloading-files"
|
||||||
|
@ -19,3 +28,27 @@ AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING = {
|
||||||
"spaceComma": "SPACE_COMMA",
|
"spaceComma": "SPACE_COMMA",
|
||||||
"spacePeriod": "SPACE_PERIOD",
|
"spacePeriod": "SPACE_PERIOD",
|
||||||
}
|
}
|
||||||
|
AIRTABLE_DURATION_FIELD_DURATION_FORMAT_MAPPING = {
|
||||||
|
"h:mm": H_M,
|
||||||
|
"h:mm:ss": H_M_S,
|
||||||
|
"h:mm:ss.s": H_M_S_S,
|
||||||
|
"h:mm:ss.ss": H_M_S_SS,
|
||||||
|
"h:mm:ss.sss": H_M_S_SSS,
|
||||||
|
}
|
||||||
|
# All colors from the rating field in Airtable: yellow, orange, red, pink, purple,
|
||||||
|
# blue, cyan, teal, green, gray. We're only mapping the ones that we have an
|
||||||
|
# alternative for.
|
||||||
|
AIRTABLE_RATING_COLOR_MAPPING = {
|
||||||
|
"blue": "dark-blue",
|
||||||
|
"green": "dark-green",
|
||||||
|
"orange": "dark-orange",
|
||||||
|
"red": "dark-red",
|
||||||
|
}
|
||||||
|
# All icons from Airtable: star, heart, thumbsUp, flag, dot. We're only mapping the
|
||||||
|
# ones that we have an alternative for.
|
||||||
|
AIRTABLE_RATING_ICON_MAPPING = {
|
||||||
|
"star": "star",
|
||||||
|
"heart": "heart",
|
||||||
|
"thumbsUp": "thumbs-up",
|
||||||
|
"flag": "flag",
|
||||||
|
}
|
||||||
|
|
|
@ -8,3 +8,10 @@ class AirtableShareIsNotABase(Exception):
|
||||||
|
|
||||||
class AirtableImportNotRespectingConfig(Exception):
|
class AirtableImportNotRespectingConfig(Exception):
|
||||||
"""Raised when the Airtable import is not respecting the `AirtableImportConfig`."""
|
"""Raised when the Airtable import is not respecting the `AirtableImportConfig`."""
|
||||||
|
|
||||||
|
|
||||||
|
class AirtableSkipCellValue(Exception):
|
||||||
|
"""
|
||||||
|
Raised when an Airtable cell value must be skipped, and be omitted from the
|
||||||
|
export.
|
||||||
|
"""
|
||||||
|
|
|
@ -39,6 +39,7 @@ from .exceptions import (
|
||||||
AirtableBaseNotPublic,
|
AirtableBaseNotPublic,
|
||||||
AirtableImportNotRespectingConfig,
|
AirtableImportNotRespectingConfig,
|
||||||
AirtableShareIsNotABase,
|
AirtableShareIsNotABase,
|
||||||
|
AirtableSkipCellValue,
|
||||||
)
|
)
|
||||||
from .import_report import (
|
from .import_report import (
|
||||||
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
@ -247,6 +248,7 @@ class AirtableHandler:
|
||||||
baserow_field.pk = 0
|
baserow_field.pk = 0
|
||||||
baserow_field.name = column["name"]
|
baserow_field.name = column["name"]
|
||||||
baserow_field.order = order
|
baserow_field.order = order
|
||||||
|
baserow_field.description = column.get("description", None) or None
|
||||||
baserow_field.primary = (
|
baserow_field.primary = (
|
||||||
baserow_field_type.can_be_primary_field(baserow_field)
|
baserow_field_type.can_be_primary_field(baserow_field)
|
||||||
and table["primaryColumnId"] == column["id"]
|
and table["primaryColumnId"] == column["id"]
|
||||||
|
@ -305,25 +307,42 @@ class AirtableHandler:
|
||||||
# Some empty rows don't have the `cellValuesByColumnId` property because it
|
# Some empty rows don't have the `cellValuesByColumnId` property because it
|
||||||
# doesn't contain values, hence the fallback to prevent failing hard.
|
# doesn't contain values, hence the fallback to prevent failing hard.
|
||||||
cell_values = row.get("cellValuesByColumnId", {})
|
cell_values = row.get("cellValuesByColumnId", {})
|
||||||
for column_id, column_value in cell_values.items():
|
|
||||||
if column_id not in column_mapping:
|
|
||||||
continue
|
|
||||||
|
|
||||||
mapping_values = column_mapping[column_id]
|
for column_id, mapping_values in column_mapping.items():
|
||||||
baserow_serialized_value = mapping_values[
|
airtable_column_type = mapping_values["airtable_column_type"]
|
||||||
"airtable_column_type"
|
args = [
|
||||||
].to_baserow_export_serialized_value(
|
|
||||||
row_id_mapping,
|
row_id_mapping,
|
||||||
table,
|
table,
|
||||||
row,
|
row,
|
||||||
mapping_values["raw_airtable_column"],
|
mapping_values["raw_airtable_column"],
|
||||||
mapping_values["baserow_field"],
|
mapping_values["baserow_field"],
|
||||||
column_value,
|
cell_values.get(column_id, None),
|
||||||
files_to_download,
|
files_to_download,
|
||||||
config,
|
config,
|
||||||
import_report,
|
import_report,
|
||||||
)
|
]
|
||||||
exported_row[f"field_{column_id}"] = baserow_serialized_value
|
|
||||||
|
try:
|
||||||
|
# The column_id typically doesn't exist in the `cell_values` if the
|
||||||
|
# value is empty in Airtable.
|
||||||
|
if column_id in cell_values:
|
||||||
|
baserow_serialized_value = (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(*args)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# remove the cell_value because that one is not accepted in the args
|
||||||
|
# of this method.
|
||||||
|
args.pop(5)
|
||||||
|
baserow_serialized_value = (
|
||||||
|
airtable_column_type.to_baserow_export_empty_value(*args)
|
||||||
|
)
|
||||||
|
exported_row[f"field_{column_id}"] = baserow_serialized_value
|
||||||
|
except AirtableSkipCellValue:
|
||||||
|
# If the `AirtableSkipCellValue` is raised, then the cell value must
|
||||||
|
# not be included in the export. This is the default behavior for
|
||||||
|
# `to_baserow_export_empty_value`, but in some cases, a specific empty
|
||||||
|
# value must be returned.
|
||||||
|
pass
|
||||||
|
|
||||||
return exported_row
|
return exported_row
|
||||||
|
|
||||||
|
@ -500,6 +519,13 @@ class AirtableHandler:
|
||||||
).can_be_primary_field(value["baserow_field"]):
|
).can_be_primary_field(value["baserow_field"]):
|
||||||
value["baserow_field"].primary = True
|
value["baserow_field"].primary = True
|
||||||
found_existing_field = True
|
found_existing_field = True
|
||||||
|
import_report.add_failed(
|
||||||
|
value["baserow_field"].name,
|
||||||
|
SCOPE_FIELD,
|
||||||
|
table["name"],
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"""Changed primary field to "{value["baserow_field"].name}" because the original primary field is incompatible.""",
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# If none of the existing fields can be primary, we will add a new
|
# If none of the existing fields can be primary, we will add a new
|
||||||
|
@ -524,6 +550,13 @@ class AirtableHandler:
|
||||||
"raw_airtable_column": airtable_column,
|
"raw_airtable_column": airtable_column,
|
||||||
"airtable_column_type": airtable_column_type,
|
"airtable_column_type": airtable_column_type,
|
||||||
}
|
}
|
||||||
|
import_report.add_failed(
|
||||||
|
baserow_field.name,
|
||||||
|
SCOPE_FIELD,
|
||||||
|
table["name"],
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"""Created new primary field "{baserow_field.name}" because none of the provided fields are compatible.""",
|
||||||
|
)
|
||||||
|
|
||||||
# Loop over all the fields and convert them to Baserow serialized format.
|
# Loop over all the fields and convert them to Baserow serialized format.
|
||||||
exported_fields = [
|
exported_fields = [
|
||||||
|
|
|
@ -2,7 +2,12 @@ from datetime import tzinfo
|
||||||
from typing import Any, Dict, Tuple, Union
|
from typing import Any, Dict, Tuple, Union
|
||||||
|
|
||||||
from baserow.contrib.database.airtable.config import AirtableImportConfig
|
from baserow.contrib.database.airtable.config import AirtableImportConfig
|
||||||
from baserow.contrib.database.airtable.import_report import AirtableImportReport
|
from baserow.contrib.database.airtable.exceptions import AirtableSkipCellValue
|
||||||
|
from baserow.contrib.database.airtable.import_report import (
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
SCOPE_FIELD,
|
||||||
|
AirtableImportReport,
|
||||||
|
)
|
||||||
from baserow.contrib.database.fields.models import Field
|
from baserow.contrib.database.fields.models import Field
|
||||||
from baserow.core.registry import Instance, Registry
|
from baserow.core.registry import Instance, Registry
|
||||||
|
|
||||||
|
@ -70,6 +75,40 @@ class AirtableColumnType(Instance):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def to_baserow_export_empty_value(
|
||||||
|
self,
|
||||||
|
row_id_mapping: Dict[str, Dict[str, int]],
|
||||||
|
raw_airtable_table: dict,
|
||||||
|
raw_airtable_row: dict,
|
||||||
|
raw_airtable_column: dict,
|
||||||
|
baserow_field: Field,
|
||||||
|
files_to_download: Dict[str, str],
|
||||||
|
config: AirtableImportConfig,
|
||||||
|
import_report: AirtableImportReport,
|
||||||
|
):
|
||||||
|
# By default, raise the `AirtableSkipCellValue` so that the value is not
|
||||||
|
# included in the export.
|
||||||
|
raise AirtableSkipCellValue
|
||||||
|
|
||||||
|
def add_import_report_failed_if_default_is_provided(
|
||||||
|
self,
|
||||||
|
raw_airtable_table: dict,
|
||||||
|
raw_airtable_column: dict,
|
||||||
|
import_report: AirtableImportReport,
|
||||||
|
to_human_readable_default=(lambda x: x),
|
||||||
|
):
|
||||||
|
default = raw_airtable_column.get("default", "")
|
||||||
|
if default:
|
||||||
|
default = to_human_readable_default(default)
|
||||||
|
import_report.add_failed(
|
||||||
|
raw_airtable_column["name"],
|
||||||
|
SCOPE_FIELD,
|
||||||
|
raw_airtable_table.get("name", ""),
|
||||||
|
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||||
|
f"The field was imported, but the default value "
|
||||||
|
f"{default} was dropped because that's not supported in Baserow.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AirtableColumnTypeRegistry(Registry):
|
class AirtableColumnTypeRegistry(Registry):
|
||||||
name = "airtable_column"
|
name = "airtable_column"
|
||||||
|
|
|
@ -39,3 +39,137 @@ def get_airtable_row_primary_value(table, row):
|
||||||
primary_value = row["id"]
|
primary_value = row["id"]
|
||||||
|
|
||||||
return primary_value
|
return primary_value
|
||||||
|
|
||||||
|
|
||||||
|
def quill_parse_inline(insert, attributes):
|
||||||
|
if "bold" in attributes:
|
||||||
|
insert = f"**{insert}**"
|
||||||
|
if "italic" in attributes:
|
||||||
|
insert = f"_{insert}_"
|
||||||
|
if "strike" in attributes:
|
||||||
|
insert = f"~{insert}~"
|
||||||
|
if "code" in attributes:
|
||||||
|
insert = f"`{insert}`"
|
||||||
|
if "link" in attributes:
|
||||||
|
insert = f"[{insert}]({attributes['link']})"
|
||||||
|
if isinstance(insert, object) and "mention" in insert:
|
||||||
|
insert = f"@{insert['mention'].get('userId', '')}"
|
||||||
|
|
||||||
|
return insert
|
||||||
|
|
||||||
|
|
||||||
|
def quill_wrap_block(attributes):
|
||||||
|
prepend = ""
|
||||||
|
append = ""
|
||||||
|
multi_line = False
|
||||||
|
if "header" in attributes:
|
||||||
|
prepend = "#" * attributes["header"] + " "
|
||||||
|
if "list" in attributes:
|
||||||
|
list_type = attributes["list"]
|
||||||
|
prepend = " " * attributes.get("indent", 0) * 4
|
||||||
|
if list_type == "ordered":
|
||||||
|
prepend += f"1. "
|
||||||
|
elif list_type == "bullet":
|
||||||
|
prepend += "- "
|
||||||
|
elif list_type == "unchecked":
|
||||||
|
prepend += "- [ ] "
|
||||||
|
elif list_type == "checked":
|
||||||
|
prepend += "- [x] "
|
||||||
|
if "blockquote" in attributes:
|
||||||
|
prepend = "> "
|
||||||
|
if "≈≈" in attributes:
|
||||||
|
prepend = "> "
|
||||||
|
if "code-block" in attributes:
|
||||||
|
prepend = "```\n"
|
||||||
|
append = "```\n"
|
||||||
|
multi_line = True
|
||||||
|
return prepend, append, multi_line
|
||||||
|
|
||||||
|
|
||||||
|
def quill_split_with_newlines(value):
|
||||||
|
parts = re.split(r"(\n)", value)
|
||||||
|
if parts and parts[0] == "":
|
||||||
|
parts.pop(0)
|
||||||
|
if parts and parts[-1] == "":
|
||||||
|
parts.pop()
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def quill_to_markdown(ops: list) -> str:
|
||||||
|
"""
|
||||||
|
Airtable uses the QuillJS editor for their rich text field. There is no library
|
||||||
|
to convert it in Baserow compatible markdown. This is a simple, custom written
|
||||||
|
function to convert it to Baserow compatible markdown.
|
||||||
|
|
||||||
|
The format is a bit odd because a newline entry can define how it should have been
|
||||||
|
formatted as on block level, making it a bit tricky because it's not sequential.
|
||||||
|
|
||||||
|
See the `test_quill_to_markdown_airtable_example` test for an example.
|
||||||
|
|
||||||
|
:param ops: The QuillJS delta object that must be converted to markdown.
|
||||||
|
:return: The converted markdown string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
md_output = []
|
||||||
|
# Holds everything that must be written as a line. Each entry in the ops can add to
|
||||||
|
# it until a "\n" character is detected.
|
||||||
|
current_object = ""
|
||||||
|
# Temporarily holds markdown code that has start and ending block, like with
|
||||||
|
# code "```", for example. Need to temporarily store the prepend and append values,
|
||||||
|
# so that we can add to it if it consists of multiple lines.
|
||||||
|
current_multi_line = None
|
||||||
|
|
||||||
|
def flush_line():
|
||||||
|
nonlocal md_output
|
||||||
|
nonlocal current_object
|
||||||
|
if current_object != "":
|
||||||
|
md_output.append(current_object)
|
||||||
|
current_object = ""
|
||||||
|
|
||||||
|
def flush_multi_line(current_prepend, current_append):
|
||||||
|
nonlocal current_object
|
||||||
|
nonlocal current_multi_line
|
||||||
|
if current_multi_line is not None and current_multi_line != (
|
||||||
|
current_prepend,
|
||||||
|
current_append,
|
||||||
|
):
|
||||||
|
current_object = (
|
||||||
|
current_multi_line[0] + current_object + current_multi_line[1]
|
||||||
|
)
|
||||||
|
flush_line()
|
||||||
|
current_multi_line = None
|
||||||
|
|
||||||
|
for index, op in enumerate(ops):
|
||||||
|
raw_insert = op.get("insert", "")
|
||||||
|
attributes = op.get("attributes", {})
|
||||||
|
|
||||||
|
if isinstance(raw_insert, str):
|
||||||
|
insert_lines = quill_split_with_newlines(raw_insert)
|
||||||
|
else:
|
||||||
|
insert_lines = [raw_insert]
|
||||||
|
|
||||||
|
# Break the insert by "\n" because the block formatting options should only
|
||||||
|
# refer to the previous line.
|
||||||
|
for insert_line in insert_lines:
|
||||||
|
is_new_line = insert_line == "\n"
|
||||||
|
|
||||||
|
if is_new_line:
|
||||||
|
prepend, append, multi_line = quill_wrap_block(attributes)
|
||||||
|
flush_multi_line(prepend, append)
|
||||||
|
|
||||||
|
# Starting a new multi-line block. All the following lines will be
|
||||||
|
# enclosed by the prepend and append.
|
||||||
|
if multi_line and current_multi_line is None:
|
||||||
|
current_multi_line = (prepend, append)
|
||||||
|
|
||||||
|
parsed_insert = quill_parse_inline(insert_line, attributes)
|
||||||
|
current_object += parsed_insert
|
||||||
|
|
||||||
|
if is_new_line and not multi_line:
|
||||||
|
current_object = prepend + current_object + append
|
||||||
|
flush_line()
|
||||||
|
|
||||||
|
flush_multi_line(None, None)
|
||||||
|
flush_line()
|
||||||
|
|
||||||
|
return "".join(md_output).strip()
|
||||||
|
|
|
@ -615,6 +615,7 @@ class DatabaseConfig(AppConfig):
|
||||||
webhook_event_type_registry.register(ViewDeletedEventType())
|
webhook_event_type_registry.register(ViewDeletedEventType())
|
||||||
|
|
||||||
from .airtable.airtable_column_types import (
|
from .airtable.airtable_column_types import (
|
||||||
|
AutoNumberAirtableColumnType,
|
||||||
CheckboxAirtableColumnType,
|
CheckboxAirtableColumnType,
|
||||||
CountAirtableColumnType,
|
CountAirtableColumnType,
|
||||||
DateAirtableColumnType,
|
DateAirtableColumnType,
|
||||||
|
@ -645,6 +646,7 @@ class DatabaseConfig(AppConfig):
|
||||||
airtable_column_type_registry.register(MultipleAttachmentAirtableColumnType())
|
airtable_column_type_registry.register(MultipleAttachmentAirtableColumnType())
|
||||||
airtable_column_type_registry.register(RichTextTextAirtableColumnType())
|
airtable_column_type_registry.register(RichTextTextAirtableColumnType())
|
||||||
airtable_column_type_registry.register(CountAirtableColumnType())
|
airtable_column_type_registry.register(CountAirtableColumnType())
|
||||||
|
airtable_column_type_registry.register(AutoNumberAirtableColumnType())
|
||||||
|
|
||||||
from .data_sync.data_sync_types import (
|
from .data_sync.data_sync_types import (
|
||||||
ICalCalendarDataSyncType,
|
ICalCalendarDataSyncType,
|
||||||
|
|
|
@ -1923,7 +1923,7 @@ class DurationFieldType(FieldType):
|
||||||
_db_column_fields = []
|
_db_column_fields = []
|
||||||
|
|
||||||
def get_model_field(self, instance: DurationField, **kwargs):
|
def get_model_field(self, instance: DurationField, **kwargs):
|
||||||
return DurationModelField(instance.duration_format, null=True)
|
return DurationModelField(instance.duration_format, null=True, **kwargs)
|
||||||
|
|
||||||
def get_serializer_field(self, instance: DurationField, **kwargs):
|
def get_serializer_field(self, instance: DurationField, **kwargs):
|
||||||
return DurationFieldSerializer(
|
return DurationFieldSerializer(
|
||||||
|
|
|
@ -54,12 +54,14 @@
|
||||||
{
|
{
|
||||||
"id":"fldG9y88Zw7q7u4Z7i4",
|
"id":"fldG9y88Zw7q7u4Z7i4",
|
||||||
"name":"Name",
|
"name":"Name",
|
||||||
"type":"text"
|
"type":"text",
|
||||||
|
"description": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":"fldB7wkyR0buF1sRF9O",
|
"id":"fldB7wkyR0buF1sRF9O",
|
||||||
"name":"Email",
|
"name":"Email",
|
||||||
"type":"text",
|
"type":"text",
|
||||||
|
"description": "This is an email",
|
||||||
"typeOptions":{
|
"typeOptions":{
|
||||||
"validatorName":"email"
|
"validatorName":"email"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ from baserow.contrib.database.airtable.airtable_column_types import (
|
||||||
TextAirtableColumnType,
|
TextAirtableColumnType,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.airtable.config import AirtableImportConfig
|
from baserow.contrib.database.airtable.config import AirtableImportConfig
|
||||||
|
from baserow.contrib.database.airtable.exceptions import AirtableSkipCellValue
|
||||||
from baserow.contrib.database.airtable.import_report import (
|
from baserow.contrib.database.airtable.import_report import (
|
||||||
SCOPE_CELL,
|
SCOPE_CELL,
|
||||||
SCOPE_FIELD,
|
SCOPE_FIELD,
|
||||||
|
@ -25,10 +26,12 @@ from baserow.contrib.database.airtable.import_report import (
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.airtable.registry import airtable_column_type_registry
|
from baserow.contrib.database.airtable.registry import airtable_column_type_registry
|
||||||
from baserow.contrib.database.fields.models import (
|
from baserow.contrib.database.fields.models import (
|
||||||
|
AutonumberField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
CountField,
|
CountField,
|
||||||
CreatedOnField,
|
CreatedOnField,
|
||||||
DateField,
|
DateField,
|
||||||
|
DurationField,
|
||||||
EmailField,
|
EmailField,
|
||||||
FileField,
|
FileField,
|
||||||
LastModifiedField,
|
LastModifiedField,
|
||||||
|
@ -42,6 +45,7 @@ from baserow.contrib.database.fields.models import (
|
||||||
TextField,
|
TextField,
|
||||||
URLField,
|
URLField,
|
||||||
)
|
)
|
||||||
|
from baserow.contrib.database.fields.utils.duration import D_H, H_M, H_M_S
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -87,6 +91,47 @@ def test_airtable_import_text_column(data_fixture, api_client):
|
||||||
"name": "Single line text",
|
"name": "Single line text",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
}
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert baserow_field.text_default == ""
|
||||||
|
assert len(import_report.items) == 0
|
||||||
|
assert isinstance(baserow_field, TextField)
|
||||||
|
assert isinstance(airtable_column_type, TextAirtableColumnType)
|
||||||
|
|
||||||
|
with pytest.raises(AirtableSkipCellValue):
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_empty_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
== ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_text_column_preserve_default(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldwSc9PqedIhTSqhi5",
|
||||||
|
"name": "Single line text",
|
||||||
|
"type": "text",
|
||||||
|
"default": "test",
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
(
|
(
|
||||||
baserow_field,
|
baserow_field,
|
||||||
airtable_column_type,
|
airtable_column_type,
|
||||||
|
@ -96,8 +141,22 @@ def test_airtable_import_text_column(data_fixture, api_client):
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
AirtableImportReport(),
|
AirtableImportReport(),
|
||||||
)
|
)
|
||||||
assert isinstance(baserow_field, TextField)
|
assert baserow_field.text_default == "test"
|
||||||
assert isinstance(airtable_column_type, TextAirtableColumnType)
|
assert len(import_report.items) == 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_empty_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
== ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -109,6 +168,7 @@ def test_airtable_import_checkbox_column(data_fixture, api_client):
|
||||||
"type": "checkbox",
|
"type": "checkbox",
|
||||||
"typeOptions": {"color": "green", "icon": "check"},
|
"typeOptions": {"color": "green", "icon": "check"},
|
||||||
}
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
(
|
(
|
||||||
baserow_field,
|
baserow_field,
|
||||||
airtable_column_type,
|
airtable_column_type,
|
||||||
|
@ -116,12 +176,89 @@ def test_airtable_import_checkbox_column(data_fixture, api_client):
|
||||||
{},
|
{},
|
||||||
airtable_field,
|
airtable_field,
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
AirtableImportReport(),
|
import_report,
|
||||||
)
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
assert isinstance(baserow_field, BooleanField)
|
assert isinstance(baserow_field, BooleanField)
|
||||||
assert isinstance(airtable_column_type, CheckboxAirtableColumnType)
|
assert isinstance(airtable_column_type, CheckboxAirtableColumnType)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_checkbox_column_with_default_value(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldTn59fpliSFcwpFA9",
|
||||||
|
"name": "Checkbox",
|
||||||
|
"type": "checkbox",
|
||||||
|
"typeOptions": {"color": "green", "icon": "check"},
|
||||||
|
"default": True,
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Checkbox"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_checkbox_column_invalid_icon(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldp1IFu0zdgRy70RoX",
|
||||||
|
"name": "Checkbox",
|
||||||
|
"type": "checkbox",
|
||||||
|
"typeOptions": {"color": "green", "icon": "TEST"},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Checkbox"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_checkbox_column_invalid_color(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldp1IFu0zdgRy70RoX",
|
||||||
|
"name": "Checkbox",
|
||||||
|
"type": "checkbox",
|
||||||
|
"typeOptions": {"color": "TEST", "icon": "check"},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Checkbox"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_created_on_column(data_fixture, api_client):
|
def test_airtable_import_created_on_column(data_fixture, api_client):
|
||||||
|
@ -156,9 +293,7 @@ def test_airtable_import_created_on_column(data_fixture, api_client):
|
||||||
assert baserow_field.date_include_time is False
|
assert baserow_field.date_include_time is False
|
||||||
assert baserow_field.date_time_format == "24"
|
assert baserow_field.date_time_format == "24"
|
||||||
assert baserow_field.date_force_timezone is None
|
assert baserow_field.date_force_timezone is None
|
||||||
assert len(import_report.items) == 1
|
assert len(import_report.items) == 0
|
||||||
assert import_report.items[0].object_name == "Created"
|
|
||||||
assert import_report.items[0].scope == SCOPE_FIELD
|
|
||||||
|
|
||||||
airtable_field = {
|
airtable_field = {
|
||||||
"id": "fldcTpJuoUVpsDNoszO",
|
"id": "fldcTpJuoUVpsDNoszO",
|
||||||
|
@ -232,9 +367,7 @@ def test_airtable_import_date_column(data_fixture, api_client):
|
||||||
assert baserow_field.date_format == "US"
|
assert baserow_field.date_format == "US"
|
||||||
assert baserow_field.date_include_time is False
|
assert baserow_field.date_include_time is False
|
||||||
assert baserow_field.date_time_format == "24"
|
assert baserow_field.date_time_format == "24"
|
||||||
assert len(import_report.items) == 1
|
assert len(import_report.items) == 0
|
||||||
assert import_report.items[0].object_name == "ISO DATE"
|
|
||||||
assert import_report.items[0].scope == SCOPE_FIELD
|
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
airtable_column_type.to_baserow_export_serialized_value(
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
@ -585,6 +718,37 @@ def test_airtable_import_datetime_edge_case_1(data_fixture, api_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_datetime_with_default_value(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldEB5dp0mNjVZu0VJI",
|
||||||
|
"name": "Date",
|
||||||
|
"type": "date",
|
||||||
|
"default": "test",
|
||||||
|
"typeOptions": {
|
||||||
|
"isDateTime": True,
|
||||||
|
"dateFormat": "Local",
|
||||||
|
"timeFormat": "24hour",
|
||||||
|
"timeZone": "client",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Date"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_email_column(data_fixture, api_client):
|
def test_airtable_import_email_column(data_fixture, api_client):
|
||||||
|
@ -810,9 +974,7 @@ def test_airtable_import_last_modified_column(data_fixture, api_client):
|
||||||
assert baserow_field.date_include_time is False
|
assert baserow_field.date_include_time is False
|
||||||
assert baserow_field.date_time_format == "24"
|
assert baserow_field.date_time_format == "24"
|
||||||
assert baserow_field.date_force_timezone is None
|
assert baserow_field.date_force_timezone is None
|
||||||
assert len(import_report.items) == 1
|
assert len(import_report.items) == 0
|
||||||
assert import_report.items[0].object_name == "Last"
|
|
||||||
assert import_report.items[0].scope == SCOPE_FIELD
|
|
||||||
|
|
||||||
airtable_field = {
|
airtable_field = {
|
||||||
"id": "fldws6n8xdrEJrMxJFJ",
|
"id": "fldws6n8xdrEJrMxJFJ",
|
||||||
|
@ -865,6 +1027,52 @@ def test_airtable_import_last_modified_column(data_fixture, api_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_last_modified_column_depending_fields(
|
||||||
|
data_fixture, api_client
|
||||||
|
):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldws6n8xdrEJrMxJFJ",
|
||||||
|
"name": "Last",
|
||||||
|
"type": "formula",
|
||||||
|
"typeOptions": {
|
||||||
|
"isDateTime": False,
|
||||||
|
"dateFormat": "Local",
|
||||||
|
"displayType": "lastModifiedTime",
|
||||||
|
"timeZone": "client",
|
||||||
|
"formulaTextParsed": "LAST_MODIFIED_TIME()",
|
||||||
|
"dependencies": {
|
||||||
|
"referencedColumnIdsForValue": [],
|
||||||
|
"referencedColumnIdsForModification": ["fld123445678"],
|
||||||
|
"dependsOnAllColumnModifications": False,
|
||||||
|
},
|
||||||
|
"resultType": "date",
|
||||||
|
"resultIsArray": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert isinstance(baserow_field, LastModifiedField)
|
||||||
|
assert isinstance(airtable_column_type, FormulaAirtableColumnType)
|
||||||
|
assert baserow_field.date_format == "ISO"
|
||||||
|
assert baserow_field.date_include_time is False
|
||||||
|
assert baserow_field.date_time_format == "24"
|
||||||
|
assert baserow_field.date_force_timezone is None
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Last"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_foreign_key_column(data_fixture, api_client):
|
def test_airtable_import_foreign_key_column(data_fixture, api_client):
|
||||||
|
@ -879,15 +1087,14 @@ def test_airtable_import_foreign_key_column(data_fixture, api_client):
|
||||||
"symmetricColumnId": "fldFh5wIL430N62LN6t",
|
"symmetricColumnId": "fldFh5wIL430N62LN6t",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
(
|
(
|
||||||
baserow_field,
|
baserow_field,
|
||||||
airtable_column_type,
|
airtable_column_type,
|
||||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
{"id": "tblxxx"},
|
{"id": "tblxxx"}, airtable_field, AirtableImportConfig(), import_report
|
||||||
airtable_field,
|
|
||||||
AirtableImportConfig(),
|
|
||||||
AirtableImportReport(),
|
|
||||||
)
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
assert isinstance(baserow_field, LinkRowField)
|
assert isinstance(baserow_field, LinkRowField)
|
||||||
assert isinstance(airtable_column_type, ForeignKeyAirtableColumnType)
|
assert isinstance(airtable_column_type, ForeignKeyAirtableColumnType)
|
||||||
assert baserow_field.link_row_table_id == "tblRpq315qnnIcg5IjI"
|
assert baserow_field.link_row_table_id == "tblRpq315qnnIcg5IjI"
|
||||||
|
@ -977,6 +1184,49 @@ def test_airtable_import_foreign_key_column(data_fixture, api_client):
|
||||||
assert baserow_field.link_row_related_field_id == "fldQcEaGEe7xuhUEuPL"
|
assert baserow_field.link_row_related_field_id == "fldQcEaGEe7xuhUEuPL"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_foreign_key_column_failed_import(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldQcEaGEe7xuhUEuPL",
|
||||||
|
"name": "Link to Users",
|
||||||
|
"type": "foreignKey",
|
||||||
|
"typeOptions": {
|
||||||
|
"foreignTableId": "tblRpq315qnnIcg5IjI",
|
||||||
|
"relationship": "one",
|
||||||
|
"unreversed": True,
|
||||||
|
"symmetricColumnId": "fldFh5wIL430N62LN6t",
|
||||||
|
"viewIdForRecordSelection": "vw1234",
|
||||||
|
"filtersForRecordSelection": [None],
|
||||||
|
"aiMatchingOptions": {"isAutoFillEnabled": False},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{"id": "tblxxx"}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 4
|
||||||
|
assert import_report.items[0].object_name == "Link to Users"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
assert import_report.items[1].object_name == "Link to Users"
|
||||||
|
assert import_report.items[1].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[1].table == ""
|
||||||
|
assert import_report.items[2].object_name == "Link to Users"
|
||||||
|
assert import_report.items[2].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[2].table == ""
|
||||||
|
assert import_report.items[3].object_name == "Link to Users"
|
||||||
|
assert import_report.items[3].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[3].table == ""
|
||||||
|
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 == "fldFh5wIL430N62LN6t"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_multiline_text_column(data_fixture, api_client):
|
def test_airtable_import_multiline_text_column(data_fixture, api_client):
|
||||||
|
@ -1032,6 +1282,7 @@ def test_airtable_import_rich_text_column(data_fixture, api_client):
|
||||||
)
|
)
|
||||||
assert isinstance(baserow_field, LongTextField)
|
assert isinstance(baserow_field, LongTextField)
|
||||||
assert isinstance(airtable_column_type, RichTextTextAirtableColumnType)
|
assert isinstance(airtable_column_type, RichTextTextAirtableColumnType)
|
||||||
|
assert baserow_field.long_text_enable_rich_text is True
|
||||||
|
|
||||||
content = {
|
content = {
|
||||||
"otDocumentId": "otdHtbNg2tJKWj62WMn",
|
"otDocumentId": "otdHtbNg2tJKWj62WMn",
|
||||||
|
@ -1043,20 +1294,19 @@ def test_airtable_import_rich_text_column(data_fixture, api_client):
|
||||||
{"insert": " cubilia curae; Class aptent taciti sociosqu ad litora."},
|
{"insert": " cubilia curae; Class aptent taciti sociosqu ad litora."},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
assert (
|
assert airtable_column_type.to_baserow_export_serialized_value(
|
||||||
airtable_column_type.to_baserow_export_serialized_value(
|
{},
|
||||||
{},
|
{"name": "Test"},
|
||||||
{"name": "Test"},
|
{"id": "row1"},
|
||||||
{"id": "row1"},
|
airtable_field,
|
||||||
airtable_field,
|
baserow_field,
|
||||||
baserow_field,
|
content,
|
||||||
content,
|
{},
|
||||||
{},
|
AirtableImportConfig(),
|
||||||
AirtableImportConfig(),
|
AirtableImportReport(),
|
||||||
AirtableImportReport(),
|
) == (
|
||||||
)
|
"**Vestibulum** ante ipsum primis in faucibus orci luctus et ultrices "
|
||||||
== "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere "
|
"_posuere_ cubilia curae; Class aptent taciti sociosqu ad litora."
|
||||||
"cubilia curae; Class aptent taciti sociosqu ad litora."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1079,6 +1329,7 @@ def test_airtable_import_rich_text_column_with_mention(data_fixture, api_client)
|
||||||
)
|
)
|
||||||
assert isinstance(baserow_field, LongTextField)
|
assert isinstance(baserow_field, LongTextField)
|
||||||
assert isinstance(airtable_column_type, RichTextTextAirtableColumnType)
|
assert isinstance(airtable_column_type, RichTextTextAirtableColumnType)
|
||||||
|
assert baserow_field.long_text_enable_rich_text is True
|
||||||
|
|
||||||
content = {
|
content = {
|
||||||
"otDocumentId": "otdHtbNg2tJKWj62WMn",
|
"otDocumentId": "otdHtbNg2tJKWj62WMn",
|
||||||
|
@ -1108,7 +1359,7 @@ def test_airtable_import_rich_text_column_with_mention(data_fixture, api_client)
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
AirtableImportReport(),
|
AirtableImportReport(),
|
||||||
) == (
|
) == (
|
||||||
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices "
|
"**Vestibulum** ante ipsum primis in faucibus orci luctus et ultrices "
|
||||||
"@usrr5CVJ5Lz8ErVZS cubilia curae; Class aptent taciti sociosqu ad litora."
|
"@usrr5CVJ5Lz8ErVZS cubilia curae; Class aptent taciti sociosqu ad litora."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1170,6 +1421,50 @@ def test_airtable_import_multi_select_column(
|
||||||
assert select_options[1].order == 0
|
assert select_options[1].order == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_multi_select_column_with_default_value(
|
||||||
|
data_fixture, api_client, django_assert_num_queries
|
||||||
|
):
|
||||||
|
table = data_fixture.create_database_table()
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldURNo0cvi6YWYcYj1",
|
||||||
|
"name": "Multiple select",
|
||||||
|
"type": "multiSelect",
|
||||||
|
"default": ["selEOJmenvqEd6pndFQ", "sel5ekvuoNVvl03olMO"],
|
||||||
|
"typeOptions": {
|
||||||
|
"choiceOrder": ["sel5ekvuoNVvl03olMO", "selEOJmenvqEd6pndFQ"],
|
||||||
|
"choices": {
|
||||||
|
"selEOJmenvqEd6pndFQ": {
|
||||||
|
"id": "selEOJmenvqEd6pndFQ",
|
||||||
|
"color": "blue",
|
||||||
|
"name": "Option 1",
|
||||||
|
},
|
||||||
|
"sel5ekvuoNVvl03olMO": {
|
||||||
|
"id": "sel5ekvuoNVvl03olMO",
|
||||||
|
"color": "cyan",
|
||||||
|
"name": "Option 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"disableColors": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Multiple select"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_number_integer_column(data_fixture, api_client):
|
def test_airtable_import_number_integer_column(data_fixture, api_client):
|
||||||
|
@ -1556,7 +1851,7 @@ def test_airtable_import_currency_column_non_existing_separator_format(
|
||||||
import_report,
|
import_report,
|
||||||
)
|
)
|
||||||
assert len(import_report.items) == 1
|
assert len(import_report.items) == 1
|
||||||
assert import_report.items[0].object_name == 'Number field: "Currency"'
|
assert import_report.items[0].object_name == "Currency"
|
||||||
assert import_report.items[0].scope == SCOPE_FIELD
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
assert import_report.items[0].table == ""
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
@ -1663,6 +1958,219 @@ def test_airtable_import_percentage_column(data_fixture, api_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_number_column_default_value(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldZBmr4L45mhjILhlA",
|
||||||
|
"name": "Number",
|
||||||
|
"type": "number",
|
||||||
|
"default": 1,
|
||||||
|
"typeOptions": {},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Number"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_days_duration_column(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldZBmr4L45mhjILhlA",
|
||||||
|
"name": "Duration",
|
||||||
|
"type": "number",
|
||||||
|
"typeOptions": {
|
||||||
|
"format": "durationInDays",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
|
assert isinstance(baserow_field, DurationField)
|
||||||
|
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||||
|
assert baserow_field.duration_format == D_H
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
1,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
== 86400 # 1 * 60 * 60 * 24
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_duration_column(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldZBmr4L45mhjILhlA",
|
||||||
|
"name": "Duration",
|
||||||
|
"type": "number",
|
||||||
|
"typeOptions": {"format": "duration", "durationFormat": "h:mm"},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
|
assert isinstance(baserow_field, DurationField)
|
||||||
|
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||||
|
assert baserow_field.duration_format == H_M
|
||||||
|
|
||||||
|
airtable_field["typeOptions"]["durationFormat"] = "h:mm:ss"
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
assert baserow_field.duration_format == H_M_S
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
1,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_duration_column_max_value(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldZBmr4L45mhjILhlA",
|
||||||
|
"name": "Duration",
|
||||||
|
"type": "number",
|
||||||
|
"typeOptions": {"format": "duration", "durationFormat": "h:mm"},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
86399999913601,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == 'Row: "row1", field: "Duration"'
|
||||||
|
assert import_report.items[0].scope == SCOPE_CELL
|
||||||
|
assert import_report.items[0].table == "Test"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_duration_column_max_negative_value(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldZBmr4L45mhjILhlA",
|
||||||
|
"name": "Duration",
|
||||||
|
"type": "number",
|
||||||
|
"typeOptions": {"format": "duration", "durationFormat": "h:mm"},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
-86399999913601,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == 'Row: "row1", field: "Duration"'
|
||||||
|
assert import_report.items[0].scope == SCOPE_CELL
|
||||||
|
assert import_report.items[0].table == "Test"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_phone_column(data_fixture, api_client):
|
def test_airtable_import_phone_column(data_fixture, api_client):
|
||||||
|
@ -1721,8 +2229,9 @@ def test_airtable_import_rating_column(data_fixture, api_client):
|
||||||
"id": "fldp1IFu0zdgRy70RoX",
|
"id": "fldp1IFu0zdgRy70RoX",
|
||||||
"name": "Rating",
|
"name": "Rating",
|
||||||
"type": "rating",
|
"type": "rating",
|
||||||
"typeOptions": {"color": "yellow", "icon": "star", "max": 5},
|
"typeOptions": {"color": "blue", "icon": "heart", "max": 5},
|
||||||
}
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
(
|
(
|
||||||
baserow_field,
|
baserow_field,
|
||||||
airtable_column_type,
|
airtable_column_type,
|
||||||
|
@ -1730,11 +2239,14 @@ def test_airtable_import_rating_column(data_fixture, api_client):
|
||||||
{},
|
{},
|
||||||
airtable_field,
|
airtable_field,
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
AirtableImportReport(),
|
import_report,
|
||||||
)
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
assert isinstance(baserow_field, RatingField)
|
assert isinstance(baserow_field, RatingField)
|
||||||
assert isinstance(airtable_column_type, RatingAirtableColumnType)
|
assert isinstance(airtable_column_type, RatingAirtableColumnType)
|
||||||
assert baserow_field.max_value == 5
|
assert baserow_field.max_value == 5
|
||||||
|
assert baserow_field.color == "dark-blue"
|
||||||
|
assert baserow_field.style == "heart"
|
||||||
assert (
|
assert (
|
||||||
airtable_column_type.to_baserow_export_serialized_value(
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
{},
|
{},
|
||||||
|
@ -1745,12 +2257,68 @@ def test_airtable_import_rating_column(data_fixture, api_client):
|
||||||
5,
|
5,
|
||||||
{},
|
{},
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
AirtableImportReport(),
|
import_report,
|
||||||
)
|
)
|
||||||
== 5
|
== 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_rating_column_invalid_icon(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldp1IFu0zdgRy70RoX",
|
||||||
|
"name": "Rating",
|
||||||
|
"type": "rating",
|
||||||
|
"typeOptions": {"color": "blue", "icon": "TEST", "max": 5},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Rating"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
assert baserow_field.max_value == 5
|
||||||
|
assert baserow_field.color == "dark-blue"
|
||||||
|
assert baserow_field.style == "star"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_rating_column_invalid_color(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldp1IFu0zdgRy70RoX",
|
||||||
|
"name": "Rating",
|
||||||
|
"type": "rating",
|
||||||
|
"typeOptions": {"color": "TEST", "icon": "heart", "max": 5},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Rating"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
assert baserow_field.max_value == 5
|
||||||
|
assert baserow_field.color == "dark-blue"
|
||||||
|
assert baserow_field.style == "heart"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_select_column(
|
def test_airtable_import_select_column(
|
||||||
|
@ -1808,6 +2376,47 @@ def test_airtable_import_select_column(
|
||||||
assert select_options[1].order == 1
|
assert select_options[1].order == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_select_column_with_default_value(
|
||||||
|
data_fixture, api_client, django_assert_num_queries
|
||||||
|
):
|
||||||
|
table = data_fixture.create_database_table()
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldRd2Vkzgsf6X4z6B4",
|
||||||
|
"name": "Single select",
|
||||||
|
"type": "select",
|
||||||
|
"default": "selbh6rEWaaiyQvWyfg",
|
||||||
|
"typeOptions": {
|
||||||
|
"choiceOrder": ["selbh6rEWaaiyQvWyfg", "selvZgpWhbkeRVphROT"],
|
||||||
|
"choices": {
|
||||||
|
"selbh6rEWaaiyQvWyfg": {
|
||||||
|
"id": "selbh6rEWaaiyQvWyfg",
|
||||||
|
"color": "blue",
|
||||||
|
"name": "Option A",
|
||||||
|
},
|
||||||
|
"selvZgpWhbkeRVphROT": {
|
||||||
|
"id": "selvZgpWhbkeRVphROT",
|
||||||
|
"color": "cyan",
|
||||||
|
"name": "Option B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"disableColors": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{}, airtable_field, AirtableImportConfig(), import_report
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 1
|
||||||
|
assert import_report.items[0].object_name == "Single select"
|
||||||
|
assert import_report.items[0].scope == SCOPE_FIELD
|
||||||
|
assert import_report.items[0].table == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_airtable_import_url_column(data_fixture, api_client):
|
def test_airtable_import_url_column(data_fixture, api_client):
|
||||||
|
@ -1906,3 +2515,44 @@ def test_airtable_import_count_column(data_fixture, api_client):
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@responses.activate
|
||||||
|
def test_airtable_import_autonumber_column(data_fixture, api_client):
|
||||||
|
airtable_field = {
|
||||||
|
"id": "fldG9y88Zw7q7u4Z7i4",
|
||||||
|
"name": "ID",
|
||||||
|
"type": "autoNumber",
|
||||||
|
"typeOptions": {
|
||||||
|
"maxUsedAutoNumber": 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
import_report = AirtableImportReport()
|
||||||
|
(
|
||||||
|
baserow_field,
|
||||||
|
airtable_column_type,
|
||||||
|
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||||
|
{},
|
||||||
|
airtable_field,
|
||||||
|
AirtableImportConfig(),
|
||||||
|
AirtableImportReport(),
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
|
assert isinstance(baserow_field, AutonumberField)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
airtable_column_type.to_baserow_export_serialized_value(
|
||||||
|
{},
|
||||||
|
{"name": "Test"},
|
||||||
|
{"id": "row1"},
|
||||||
|
airtable_field,
|
||||||
|
baserow_field,
|
||||||
|
1,
|
||||||
|
{},
|
||||||
|
AirtableImportConfig(),
|
||||||
|
import_report,
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
assert len(import_report.items) == 0
|
||||||
|
|
|
@ -232,7 +232,7 @@ def test_to_baserow_database_export():
|
||||||
assert baserow_database_export["tables"][1]["id"] == "tbl7glLIGtH8C8zGCzb"
|
assert baserow_database_export["tables"][1]["id"] == "tbl7glLIGtH8C8zGCzb"
|
||||||
assert baserow_database_export["tables"][1]["name"] == "Data"
|
assert baserow_database_export["tables"][1]["name"] == "Data"
|
||||||
assert baserow_database_export["tables"][1]["order"] == 1
|
assert baserow_database_export["tables"][1]["order"] == 1
|
||||||
assert len(baserow_database_export["tables"][1]["fields"]) == 25
|
assert len(baserow_database_export["tables"][1]["fields"]) == 26
|
||||||
|
|
||||||
# We don't have to check all the fields and rows, just a single one, because we have
|
# We don't have to check all the fields and rows, just a single one, because we have
|
||||||
# separate tests for mapping the Airtable fields and values to Baserow.
|
# separate tests for mapping the Airtable fields and values to Baserow.
|
||||||
|
@ -255,7 +255,7 @@ def test_to_baserow_database_export():
|
||||||
"type": "email",
|
"type": "email",
|
||||||
"id": "fldB7wkyR0buF1sRF9O",
|
"id": "fldB7wkyR0buF1sRF9O",
|
||||||
"name": "Email",
|
"name": "Email",
|
||||||
"description": None,
|
"description": "This is an email",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"read_only": False,
|
"read_only": False,
|
||||||
|
@ -435,6 +435,17 @@ def test_to_baserow_database_export_without_primary_value():
|
||||||
AirtableImportConfig(),
|
AirtableImportConfig(),
|
||||||
)
|
)
|
||||||
assert baserow_database_export["tables"][0]["fields"][0]["primary"] is True
|
assert baserow_database_export["tables"][0]["fields"][0]["primary"] is True
|
||||||
|
assert baserow_database_export["tables"][1]["rows"][0] == {
|
||||||
|
"id": 1,
|
||||||
|
"order": "1.00000000000000000000",
|
||||||
|
"created_on": None,
|
||||||
|
"updated_on": None,
|
||||||
|
"field_object_name": "Name",
|
||||||
|
"field_scope": "scope_field",
|
||||||
|
"field_table": "table_Users",
|
||||||
|
"field_error_type": "error_type_unsupported_feature",
|
||||||
|
"field_message": 'Changed primary field to "Name" because the original primary field is incompatible.',
|
||||||
|
}
|
||||||
|
|
||||||
user_table_json["data"]["tableSchemas"][0]["columns"] = []
|
user_table_json["data"]["tableSchemas"][0]["columns"] = []
|
||||||
schema, tables = AirtableHandler.extract_schema(deepcopy([user_table_json]))
|
schema, tables = AirtableHandler.extract_schema(deepcopy([user_table_json]))
|
||||||
|
@ -455,6 +466,17 @@ def test_to_baserow_database_export_without_primary_value():
|
||||||
"immutable_properties": False,
|
"immutable_properties": False,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
assert baserow_database_export["tables"][1]["rows"][0] == {
|
||||||
|
"id": 1,
|
||||||
|
"order": "1.00000000000000000000",
|
||||||
|
"created_on": None,
|
||||||
|
"updated_on": None,
|
||||||
|
"field_object_name": "Primary field (auto created)",
|
||||||
|
"field_scope": "scope_field",
|
||||||
|
"field_table": "table_Users",
|
||||||
|
"field_error_type": "error_type_unsupported_feature",
|
||||||
|
"field_message": 'Created new primary field "Primary field (auto created)" because none of the provided fields are compatible.',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
|
@ -3,6 +3,7 @@ import pytest
|
||||||
from baserow.contrib.database.airtable.utils import (
|
from baserow.contrib.database.airtable.utils import (
|
||||||
extract_share_id_from_url,
|
extract_share_id_from_url,
|
||||||
get_airtable_row_primary_value,
|
get_airtable_row_primary_value,
|
||||||
|
quill_to_markdown,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,3 +62,188 @@ def test_get_airtable_row_primary_value_without_primary_column_id_in_table():
|
||||||
}
|
}
|
||||||
airtable_row = {"id": "id1"}
|
airtable_row = {"id": "id1"}
|
||||||
assert get_airtable_row_primary_value(airtable_table, airtable_row) == "id1"
|
assert get_airtable_row_primary_value(airtable_table, airtable_row) == "id1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_quill_to_markdown_airtable_example():
|
||||||
|
markdown_value = quill_to_markdown(
|
||||||
|
[
|
||||||
|
{"insert": "He"},
|
||||||
|
{"attributes": {"bold": True}, "insert": "adi"},
|
||||||
|
{"insert": "ng 1"},
|
||||||
|
{"attributes": {"header": 1}, "insert": "\n"},
|
||||||
|
{"insert": "He"},
|
||||||
|
{"attributes": {"link": "https://airtable.com"}, "insert": "a"},
|
||||||
|
{
|
||||||
|
"attributes": {"link": "https://airtable.com", "bold": True},
|
||||||
|
"insert": "di",
|
||||||
|
},
|
||||||
|
{"attributes": {"bold": True}, "insert": "n"},
|
||||||
|
{"insert": "g 2"},
|
||||||
|
{"attributes": {"header": 2}, "insert": "\n"},
|
||||||
|
{"insert": "Heading 3"},
|
||||||
|
{"attributes": {"header": 3}, "insert": "\n"},
|
||||||
|
{"insert": "\none"},
|
||||||
|
{"attributes": {"list": "ordered"}, "insert": "\n"},
|
||||||
|
{"insert": "two"},
|
||||||
|
{"attributes": {"list": "ordered"}, "insert": "\n"},
|
||||||
|
{"insert": "three"},
|
||||||
|
{"attributes": {"list": "ordered"}, "insert": "\n"},
|
||||||
|
{"insert": "Sub 1"},
|
||||||
|
{"attributes": {"indent": 1, "list": "ordered"}, "insert": "\n"},
|
||||||
|
{"insert": "Sub 2"},
|
||||||
|
{"attributes": {"indent": 1, "list": "ordered"}, "insert": "\n"},
|
||||||
|
{"insert": "\none"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"insert": "two"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"insert": "three"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"insert": "Sub 1"},
|
||||||
|
{"attributes": {"indent": 1, "list": "bullet"}, "insert": "\n"},
|
||||||
|
{"insert": "Sub 2"},
|
||||||
|
{"attributes": {"indent": 1, "list": "bullet"}, "insert": "\n"},
|
||||||
|
{"insert": "\nCheck 1"},
|
||||||
|
{"attributes": {"list": "unchecked"}, "insert": "\n"},
|
||||||
|
{"insert": "Check 2"},
|
||||||
|
{"attributes": {"list": "unchecked"}, "insert": "\n"},
|
||||||
|
{"insert": "Check 3"},
|
||||||
|
{"insert": "\n", "attributes": {"list": "unchecked"}},
|
||||||
|
{"insert": "\nLorem "},
|
||||||
|
{"insert": "ipsum", "attributes": {"bold": True}},
|
||||||
|
{"insert": " dolor "},
|
||||||
|
{"attributes": {"italic": True}, "insert": "sit"},
|
||||||
|
{"insert": " amet, "},
|
||||||
|
{"attributes": {"strike": True}, "insert": "consectetur"},
|
||||||
|
{"insert": " adipiscing "},
|
||||||
|
{"attributes": {"code": True}, "insert": "elit"},
|
||||||
|
{"insert": ". "},
|
||||||
|
{"attributes": {"link": "https://airtable.com"}, "insert": "Proin"},
|
||||||
|
{"insert": " ut metus quam. Ut tempus at "},
|
||||||
|
{
|
||||||
|
"attributes": {"link": "https://airtable.com"},
|
||||||
|
"insert": "https://airtable.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " vel varius. Phasellus nec diam vitae urna mollis cursus. Donec mattis pellentesque nunc id dictum. Maecenas vel tortor quam. Vestibulum et enim ut mauris lacinia malesuada. Pellentesque euismod\niaculis felis, at posuere velit ullamcorper a. Aliquam eu ultricies neque, cursus accumsan metus. Etiam consectetur eu nisi id aliquet. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": {
|
||||||
|
"mention": {
|
||||||
|
"userId": "usrGIN77VWdhm7LKk",
|
||||||
|
"mentionId": "menuy6d9AwkWbnNXx",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " gravida vestibulum egestas. Praesent pretium velit eu pretium ultrices. Nullam ut est non quam vulputate "
|
||||||
|
},
|
||||||
|
{"attributes": {"code": True}, "insert": "tempus nec vel augue"},
|
||||||
|
{
|
||||||
|
"insert": ". Aenean dui velit, ornare nec tincidunt eget, fermentum sed arcu. Suspendisse consequat bibendum molestie. Fusce at pulvinar enim.\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": {
|
||||||
|
"mention": {
|
||||||
|
"userId": "usrGIN77VWdhm7LKk",
|
||||||
|
"mentionId": "menflYNpEgIJljLuR",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"insert": "\nQuote, but not actually"},
|
||||||
|
{"attributes": {"blockquote": True}, "insert": "\n"},
|
||||||
|
{"insert": "on multiple lines"},
|
||||||
|
{"attributes": {"blockquote": True}, "insert": "\n"},
|
||||||
|
{"insert": "\ncode"},
|
||||||
|
{"attributes": {"code-block": True}, "insert": "\n"},
|
||||||
|
{"insert": "line"},
|
||||||
|
{"attributes": {"code-block": True}, "insert": "\n"},
|
||||||
|
{"insert": "2"},
|
||||||
|
{"insert": "\n", "attributes": {"code-block": True}},
|
||||||
|
{"insert": "\n{"},
|
||||||
|
{"insert": "\n", "attributes": {"code-block": True}},
|
||||||
|
{"insert": ' "test": "test",'},
|
||||||
|
{"attributes": {"code-block": True}, "insert": "\n"},
|
||||||
|
{"insert": ' "yeah": "test"'},
|
||||||
|
{"attributes": {"code-block": True}, "insert": "\n"},
|
||||||
|
{"insert": "}"},
|
||||||
|
{"insert": "\n", "attributes": {"code-block": True}},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
markdown_value
|
||||||
|
== """# He**adi**ng 1
|
||||||
|
## He[a](https://airtable.com)[**di**](https://airtable.com)**n**g 2
|
||||||
|
### Heading 3
|
||||||
|
|
||||||
|
1. one
|
||||||
|
1. two
|
||||||
|
1. three
|
||||||
|
1. Sub 1
|
||||||
|
1. Sub 2
|
||||||
|
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
- three
|
||||||
|
- Sub 1
|
||||||
|
- Sub 2
|
||||||
|
|
||||||
|
- [ ] Check 1
|
||||||
|
- [ ] Check 2
|
||||||
|
- [ ] Check 3
|
||||||
|
|
||||||
|
Lorem **ipsum** dolor _sit_ amet, ~consectetur~ adipiscing `elit`. [Proin](https://airtable.com) ut metus quam. Ut tempus at [https://airtable.com](https://airtable.com) vel varius. Phasellus nec diam vitae urna mollis cursus. Donec mattis pellentesque nunc id dictum. Maecenas vel tortor quam. Vestibulum et enim ut mauris lacinia malesuada. Pellentesque euismod
|
||||||
|
iaculis felis, at posuere velit ullamcorper a. Aliquam eu ultricies neque, cursus accumsan metus. Etiam consectetur eu nisi id aliquet. @usrGIN77VWdhm7LKk gravida vestibulum egestas. Praesent pretium velit eu pretium ultrices. Nullam ut est non quam vulputate `tempus nec vel augue`. Aenean dui velit, ornare nec tincidunt eget, fermentum sed arcu. Suspendisse consequat bibendum molestie. Fusce at pulvinar enim.
|
||||||
|
@usrGIN77VWdhm7LKk
|
||||||
|
> Quote, but not actually
|
||||||
|
> on multiple lines
|
||||||
|
|
||||||
|
```
|
||||||
|
code
|
||||||
|
line
|
||||||
|
2
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"test": "test",
|
||||||
|
"yeah": "test"
|
||||||
|
}
|
||||||
|
```"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quill_to_markdown_airtable_example_two_lists():
|
||||||
|
markdown_value = quill_to_markdown(
|
||||||
|
[
|
||||||
|
{"insert": "This is great"},
|
||||||
|
{"insert": "\n", "attributes": {"header": 2}},
|
||||||
|
{"insert": "option 1"},
|
||||||
|
{"attributes": {"list": "unchecked"}, "insert": "\n"},
|
||||||
|
{"insert": "option 2"},
|
||||||
|
{"attributes": {"list": "unchecked"}, "insert": "\n"},
|
||||||
|
{"insert": "option that is "},
|
||||||
|
{"attributes": {"bold": True}, "insert": "bold"},
|
||||||
|
{"attributes": {"list": "unchecked"}, "insert": "\n"},
|
||||||
|
{"insert": "\n"},
|
||||||
|
{"attributes": {"bold": True}, "insert": "item"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"attributes": {"italic": True}, "insert": "item"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"attributes": {"strike": True}, "insert": "Item"},
|
||||||
|
{"attributes": {"list": "bullet"}, "insert": "\n"},
|
||||||
|
{"attributes": {"link": "https://airtable.com"}, "insert": "link"},
|
||||||
|
{"insert": "\n", "attributes": {"list": "bullet"}},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
markdown_value
|
||||||
|
== """## This is great
|
||||||
|
- [ ] option 1
|
||||||
|
- [ ] option 2
|
||||||
|
- [ ] option that is **bold**
|
||||||
|
|
||||||
|
- **item**
|
||||||
|
- _item_
|
||||||
|
- ~Item~
|
||||||
|
- [link](https://airtable.com)"""
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "Airtable import field type improvements.",
|
||||||
|
"issue_number": 3455,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2025-02-17"
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue