mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-15 01:28:30 +00:00
Merge branch '3193_table_export_import_decorations_value_mapping' into 'develop'
#3193 table import - properly restore decoration value ids Closes #3193 See merge request baserow/baserow!2937
This commit is contained in:
commit
01e664073e
11 changed files with 167 additions and 34 deletions
backend
src/baserow/contrib/database/views
tests/baserow/contrib/database/field
changelog/entries/unreleased/bug
premium/backend
src/baserow_premium/views
tests/baserow_premium_tests/views
web-frontend/modules/database
|
@ -943,6 +943,8 @@ class ViewFilterType(Instance):
|
|||
:rtype: str
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return ""
|
||||
return value
|
||||
|
||||
def set_import_serialized_value(self, value, id_mapping) -> str:
|
||||
|
@ -960,6 +962,8 @@ class ViewFilterType(Instance):
|
|||
:rtype: str
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
return ""
|
||||
return value
|
||||
|
||||
def field_is_compatible(self, field):
|
||||
|
|
|
@ -1147,7 +1147,11 @@ class SingleSelectIsAnyOfViewFilterType(ViewFilterType):
|
|||
def parse_option_ids(self, value):
|
||||
try:
|
||||
return [int(v) for v in value.split(",") if v.isdigit()]
|
||||
except ValueError:
|
||||
# non-strings will raise AttributeError, so we have a type check here too
|
||||
except (
|
||||
ValueError,
|
||||
AttributeError,
|
||||
):
|
||||
return []
|
||||
|
||||
def get_filter(self, field_name, value: str, model_field, field):
|
||||
|
@ -1162,18 +1166,14 @@ class SingleSelectIsAnyOfViewFilterType(ViewFilterType):
|
|||
filter_function = self.filter_functions[field_type.type]
|
||||
return filter_function(field_name, option_ids, model_field, field)
|
||||
|
||||
def set_import_serialized_value(self, value, id_mapping):
|
||||
splitted = value.split(",")
|
||||
def set_import_serialized_value(self, value: str, id_mapping: dict) -> str:
|
||||
# Parses the old option ids and remaps them to the new option ids.
|
||||
old_options_ids = self.parse_option_ids(value)
|
||||
select_option_map = id_mapping["database_field_select_options"]
|
||||
new_values = []
|
||||
for value in splitted:
|
||||
try:
|
||||
int_value = int(value)
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
new_id = str(id_mapping["database_field_select_options"].get(int_value, ""))
|
||||
new_values.append(new_id)
|
||||
|
||||
for old_id in old_options_ids:
|
||||
if new_id := select_option_map.get(old_id):
|
||||
new_values.append(str(new_id))
|
||||
return ",".join(new_values)
|
||||
|
||||
|
||||
|
@ -1429,12 +1429,16 @@ class MultipleSelectHasViewFilterType(ManyToManyHasBaseViewFilter):
|
|||
return filter_function(field_name, option_ids, model_field, field)
|
||||
|
||||
def set_import_serialized_value(self, value, id_mapping):
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
return ""
|
||||
# Parses the old option ids and remaps them to the new option ids.
|
||||
old_options_ids = self.parse_option_ids(value)
|
||||
select_option_map = id_mapping["database_field_select_options"]
|
||||
|
||||
return str(id_mapping["database_field_select_options"].get(value, ""))
|
||||
new_values = []
|
||||
for old_id in old_options_ids:
|
||||
if new_id := select_option_map.get(old_id):
|
||||
new_values.append(str(new_id))
|
||||
|
||||
return ",".join(new_values)
|
||||
|
||||
|
||||
class MultipleSelectHasNotViewFilterType(
|
||||
|
@ -1461,6 +1465,8 @@ class MultipleCollaboratorsHasViewFilterType(ManyToManyHasBaseViewFilter):
|
|||
COLLABORATORS_KEY = f"available_collaborators"
|
||||
|
||||
def get_export_serialized_value(self, value, id_mapping):
|
||||
if value is None:
|
||||
value = ""
|
||||
if self.COLLABORATORS_KEY not in id_mapping:
|
||||
workspace_id = id_mapping.get("workspace_id", None)
|
||||
if workspace_id is None:
|
||||
|
@ -1528,6 +1534,8 @@ class UserIsViewFilterType(ViewFilterType):
|
|||
return Q()
|
||||
|
||||
def get_export_serialized_value(self, value, id_mapping):
|
||||
if value is None:
|
||||
value = ""
|
||||
if self.USER_KEY not in id_mapping:
|
||||
workspace_id = id_mapping.get("workspace_id", None)
|
||||
if workspace_id is None:
|
||||
|
|
|
@ -1741,6 +1741,20 @@ def test_single_select_is_any_of_filter_type(field_name, data_fixture):
|
|||
assert set(ids) == set([o.id for o in rows[:4]])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_single_select_is_any_of_filter_type_export_import():
|
||||
view_filter_type = view_filter_type_registry.get("single_select_is_any_of")
|
||||
id_mapping = {"database_field_select_options": {1: 2, 100: 200}}
|
||||
assert view_filter_type.get_export_serialized_value("1", {}) == "1"
|
||||
assert view_filter_type.set_import_serialized_value("1", id_mapping) == "2"
|
||||
assert view_filter_type.set_import_serialized_value("", id_mapping) == ""
|
||||
assert view_filter_type.set_import_serialized_value("wrong", id_mapping) == ""
|
||||
assert view_filter_type.set_import_serialized_value("1,invalid", id_mapping) == "2"
|
||||
assert view_filter_type.set_import_serialized_value("1,100", id_mapping) == "2,200"
|
||||
assert view_filter_type.set_import_serialized_value("2,100", id_mapping) == "200"
|
||||
assert view_filter_type.set_import_serialized_value(None, id_mapping) == ""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"field_name", ["single_select", "ref_single_select", "ref_ref_single_select"]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "bug",
|
||||
"message": "Restore decoration value ids during table import",
|
||||
"issue_number": 3193,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-12-04"
|
||||
}
|
|
@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, Optional, Set, Tuple
|
|||
from uuid import uuid4
|
||||
|
||||
from baserow_premium.license.handler import LicenseHandler
|
||||
from loguru import logger
|
||||
|
||||
from baserow.contrib.database.fields.field_types import SingleSelectFieldType
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
|
@ -128,18 +129,22 @@ class ConditionalColorValueProviderType(PremiumDecoratorValueProviderType):
|
|||
if "id" not in color:
|
||||
color["id"] = str(uuid4())
|
||||
|
||||
new_filters = []
|
||||
for color_filter in color["filters"]:
|
||||
new_value = (
|
||||
id_mapping["database_field_select_options"].get(
|
||||
int(color_filter["value"])
|
||||
)
|
||||
if "database_field_select_options" in id_mapping
|
||||
and str(color_filter["value"]).isdigit()
|
||||
else color_filter["value"]
|
||||
)
|
||||
color_filter["value"] = new_value
|
||||
new_field_id = id_mapping["database_fields"][color_filter["field"]]
|
||||
color_filter["field"] = new_field_id
|
||||
try:
|
||||
filter_type = view_filter_type_registry.get(
|
||||
color_filter.get("type")
|
||||
)
|
||||
imported_value = filter_type.set_import_serialized_value(
|
||||
color_filter["value"], id_mapping
|
||||
)
|
||||
color_filter["value"] = imported_value
|
||||
new_filters.append(color_filter)
|
||||
except Exception as err:
|
||||
logger.warning(f"Cannot import filter value: {color_filter}: {err}")
|
||||
color["filters"] = new_filters
|
||||
|
||||
return value
|
||||
|
||||
|
|
|
@ -42,6 +42,11 @@ class ConditionalColorValueProviderConfColorFilterSerializer(serializers.Seriali
|
|||
),
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs.get("value") is None:
|
||||
attrs["value"] = ""
|
||||
return attrs
|
||||
|
||||
|
||||
class ConditionalColorValueProviderConfColorFilterGroupSerializer(
|
||||
serializers.Serializer
|
||||
|
|
|
@ -50,6 +50,7 @@ def test_import_export_grid_view_w_decorator(data_fixture):
|
|||
{
|
||||
"filter_groups": [{"id": 1}],
|
||||
"filters": [
|
||||
# no filter type, so it will be ignored in imported value
|
||||
{"field": field.id, "group": 1, "value": ""},
|
||||
{
|
||||
"type": "single_select_equal",
|
||||
|
@ -59,12 +60,52 @@ def test_import_export_grid_view_w_decorator(data_fixture):
|
|||
},
|
||||
],
|
||||
},
|
||||
# no filter type, so it will be ignored in imported value
|
||||
{"filters": [{"field": field.id, "value": ""}]},
|
||||
]
|
||||
},
|
||||
order=2,
|
||||
)
|
||||
|
||||
view_decoration_3 = data_fixture.create_view_decoration(
|
||||
view=grid_view,
|
||||
type="left_border_color",
|
||||
value_provider_type="conditional_color",
|
||||
value_provider_conf={
|
||||
"colors": [
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"type": "single_select_is_any_of",
|
||||
"field": single_select.id,
|
||||
"group": 1,
|
||||
"value": f"{option.id},100",
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"field": single_select.id,
|
||||
"group": 1,
|
||||
"value": f"true",
|
||||
},
|
||||
{
|
||||
"type": "invalid_filter",
|
||||
"field": single_select.id,
|
||||
"group": 1,
|
||||
"value": f"foobar",
|
||||
},
|
||||
{
|
||||
"type": "single_select_is_any_of",
|
||||
"field": single_select.id,
|
||||
"group": 1,
|
||||
"value": f"100,{option.id}",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
order=3,
|
||||
)
|
||||
|
||||
id_mapping = {
|
||||
"database_fields": {
|
||||
field.id: imported_field.id,
|
||||
|
@ -101,19 +142,64 @@ def test_import_export_grid_view_w_decorator(data_fixture):
|
|||
{
|
||||
"id": AnyStr(), # a new id is generated for every inserted color
|
||||
"filters": [
|
||||
{"field": imported_field.id, "group": 1, "value": ""},
|
||||
# first filter will be ignored, because it's missing
|
||||
# proper type value
|
||||
{
|
||||
"type": "single_select_equal",
|
||||
"field": imported_single_select.id,
|
||||
"group": 1,
|
||||
"value": imported_option.id,
|
||||
# filter value should be a string
|
||||
"value": str(imported_option.id),
|
||||
},
|
||||
],
|
||||
"filter_groups": [{"id": 1}],
|
||||
},
|
||||
{
|
||||
"id": AnyStr(),
|
||||
"filters": [{"field": imported_field.id, "value": ""}],
|
||||
# empty list because filter def is missing proper type value
|
||||
"filters": [],
|
||||
},
|
||||
]
|
||||
|
||||
assert view_decoration_3.id != imported_view_decorations[2].id
|
||||
assert view_decoration_3.type == imported_view_decorations[2].type
|
||||
assert (
|
||||
view_decoration_3.value_provider_type
|
||||
== imported_view_decorations[2].value_provider_type
|
||||
)
|
||||
|
||||
# test a list of values with one value id not present in the mapping
|
||||
assert imported_view_decorations[2].value_provider_conf["colors"] == [
|
||||
{
|
||||
"id": AnyStr(), # a new id is generated for every inserted color
|
||||
"filters": [
|
||||
{
|
||||
"type": "single_select_is_any_of",
|
||||
"field": imported_single_select.id,
|
||||
"group": 1,
|
||||
# old 100 option will be scrapped, because
|
||||
# it's not in the mapping
|
||||
"value": f"{imported_option.id}",
|
||||
},
|
||||
# boolean should not be modified
|
||||
{
|
||||
"type": "boolean",
|
||||
"field": imported_single_select.id,
|
||||
"group": 1,
|
||||
# old 100 option will be scrapped, because
|
||||
# it's not in the mapping
|
||||
"value": "true",
|
||||
},
|
||||
# no invalid_filter
|
||||
{
|
||||
"type": "single_select_is_any_of",
|
||||
"field": imported_single_select.id,
|
||||
"group": 1,
|
||||
# old 100 option will be scrapped, because
|
||||
# it's not in the mapping
|
||||
"value": f"{imported_option.id}",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -267,13 +267,15 @@ export const hasSelectOptionIdEqualMixin = Object.assign(
|
|||
const mapOptionIdsToValues = (cellVal) =>
|
||||
cellVal.map((v) => ({
|
||||
id: v.id,
|
||||
value: String(v.value?.id || ''),
|
||||
value: String(v.value?.id ?? ''),
|
||||
}))
|
||||
const hasValueEqualFilter = (cellVal, fltValue) =>
|
||||
genericHasValueEqualFilter(mapOptionIdsToValues(cellVal), fltValue)
|
||||
|
||||
return (cellValue, filterValue) => {
|
||||
const filterValues = filterValue.trim().split(',')
|
||||
const filterValues = String(filterValue ?? '')
|
||||
.trim()
|
||||
.split(',')
|
||||
return filterValues.reduce((acc, fltValue) => {
|
||||
return acc || hasValueEqualFilter(cellValue, String(fltValue))
|
||||
}, false)
|
||||
|
|
|
@ -13,7 +13,9 @@ export default {
|
|||
mixins: [viewFilter],
|
||||
computed: {
|
||||
copy() {
|
||||
const value = (this.filter.value || '').toString().toLowerCase().trim()
|
||||
const value = String(this.filter.value ?? '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
return trueValues.includes(value)
|
||||
},
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@ export default {
|
|||
mixins: [viewFilter],
|
||||
computed: {
|
||||
copy() {
|
||||
const value = this.filter.value
|
||||
const value = String(this.filter.value ?? '')
|
||||
return value
|
||||
.split(',')
|
||||
.map((value) => parseInt(value))
|
||||
|
|
|
@ -19,7 +19,7 @@ export default {
|
|||
mixins: [viewFilter],
|
||||
computed: {
|
||||
copy() {
|
||||
const value = this.filter.value
|
||||
const value = String(this.filter.value ?? '')
|
||||
return value === '' || value.includes(',')
|
||||
? null
|
||||
: parseInt(value) || null
|
||||
|
|
Loading…
Add table
Reference in a new issue