1
0
Fork 0
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'

 table import - properly restore decoration value ids

Closes 

See merge request 
This commit is contained in:
Cezary Statkiewicz 2025-01-03 17:46:58 +00:00
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
web-frontend/modules/database

View file

@ -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):

View file

@ -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:

View file

@ -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"]

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "Restore decoration value ids during table import",
"issue_number": 3193,
"bullet_points": [],
"created_at": "2024-12-04"
}

View file

@ -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

View file

@ -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

View file

@ -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}",
},
],
},
]

View file

@ -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)

View file

@ -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)
},
},

View file

@ -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))

View file

@ -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