diff --git a/specifyweb/backend/workbench/upload/column_options.py b/specifyweb/backend/workbench/upload/column_options.py index 4a9bd278387..95b8f3fbb29 100644 --- a/specifyweb/backend/workbench/upload/column_options.py +++ b/specifyweb/backend/workbench/upload/column_options.py @@ -5,6 +5,7 @@ from specifyweb.specify.utils.uiformatters import ScopedFormatter MatchBehavior = Literal["ignoreWhenBlank", "ignoreAlways", "ignoreNever"] +DisambiguationBehavior = Literal["ask", "pickFirst"] # A single row in the workbench. Maps column names to values in the row Row = dict[str, str] @@ -14,9 +15,10 @@ class ColumnOptions(NamedTuple): matchBehavior: MatchBehavior nullAllowed: bool default: str | None + disambiguationBehavior: DisambiguationBehavior def to_json(self) -> dict | str: - if self.matchBehavior == "ignoreNever" and self.nullAllowed and self.default is None: + if self.matchBehavior == "ignoreNever" and self.nullAllowed and self.default is None and self.disambiguationBehavior == "ask": return self.column return dict(self._asdict()) @@ -26,6 +28,7 @@ class ExtendedColumnOptions(NamedTuple): matchBehavior: MatchBehavior nullAllowed: bool default: str | None + disambiguationBehavior: DisambiguationBehavior uiformatter: ScopedFormatter | None schemaitem: Any picklist: Any diff --git a/specifyweb/backend/workbench/upload/parsing.py b/specifyweb/backend/workbench/upload/parsing.py index b59d6601d1c..7abcad0b1e3 100644 --- a/specifyweb/backend/workbench/upload/parsing.py +++ b/specifyweb/backend/workbench/upload/parsing.py @@ -5,7 +5,7 @@ from specifyweb.specify.datamodel import datamodel from specifyweb.backend.workbench.upload.predicates import filter_match_key -from .column_options import ExtendedColumnOptions +from .column_options import DisambiguationBehavior, ExtendedColumnOptions from specifyweb.backend.workbench.upload.parse import parse_field, is_latlong, ParseSucess, ParseFailure Row = dict[str, str] @@ -38,17 +38,20 @@ class ParseResult(NamedTuple): add_to_picklist: PicklistAddition | None column: str missing_required: str | None + disambiguation_behavior: DisambiguationBehavior @classmethod - def from_parse_success(cls, ps: ParseSucess, filter_on: Filter, add_to_picklist: PicklistAddition | None, column: str, missing_required: str | None): - return cls(filter_on=filter_on, upload=ps.payload, add_to_picklist=add_to_picklist, column=column, missing_required=missing_required) + def from_parse_success(cls, ps: ParseSucess, filter_on: Filter, add_to_picklist: PicklistAddition | None, column: str, + missing_required: str | None, disambiguation_behavior: DisambiguationBehavior): + return cls(filter_on=filter_on, upload=ps.payload, add_to_picklist=add_to_picklist, column=column, + missing_required=missing_required, disambiguation_behavior=disambiguation_behavior) def match_key(self) -> str: return filter_match_key(self.filter_on) -def filter_and_upload(f: Filter, column: str) -> ParseResult: - return ParseResult(f, f, None, column, None) +def filter_and_upload(f: Filter, column: str, disambiguation_behavior: DisambiguationBehavior = 'ask') -> ParseResult: + return ParseResult(f, f, None, column, None, disambiguation_behavior) def parse_many(tablename: str, mapping: dict[str, ExtendedColumnOptions], row: Row) -> tuple[list[ParseResult], list[WorkBenchParseFailure]]: @@ -76,7 +79,7 @@ def parse_value(tablename: str, fieldname: str, value_in: str, colopts: Extended None ) result = ParseResult({fieldname: None}, {fieldname: None}, - None, colopts.column, missing_required) + None, colopts.column, missing_required, colopts.disambiguationBehavior) else: result = _parse(tablename, fieldname, colopts, colopts.default) @@ -105,7 +108,7 @@ def _parse(tablename: str, fieldname: str, colopts: ExtendedColumnOptions, value field = table.get_field_strict(fieldname) if colopts.picklist: - result = parse_with_picklist(colopts.picklist, fieldname, value, colopts.column) + result = parse_with_picklist(colopts.picklist, fieldname, value, colopts.column, colopts.disambiguationBehavior,) if result is not None: if isinstance(result, ParseResult) and hasattr(field, 'length') and field.length is not None and len(result.upload[fieldname]) > field.length: return WorkBenchParseFailure( @@ -123,19 +126,19 @@ def _parse(tablename: str, fieldname: str, colopts: ExtendedColumnOptions, value if is_latlong(table, field) and isinstance(parsed, ParseSucess): coord_text_field = field.name.replace('itude', '') + 'text' if field.name else '' filter_on = {coord_text_field: parsed.payload[coord_text_field]} - return ParseResult.from_parse_success(parsed, filter_on, None, colopts.column, None) + return ParseResult.from_parse_success(parsed, filter_on, None, colopts.column, None, colopts.disambiguationBehavior) if isinstance(parsed, ParseFailure): return WorkBenchParseFailure.from_parse_failure(parsed, colopts.column) else: - return ParseResult.from_parse_success(parsed, parsed.payload, None, colopts.column, None) + return ParseResult.from_parse_success(parsed, parsed.payload, None, colopts.column, None, colopts.disambiguationBehavior) -def parse_with_picklist(picklist, fieldname: str, value: str, column: str) -> ParseResult | WorkBenchParseFailure | None: +def parse_with_picklist(picklist, fieldname: str, value: str, column: str, disambiguation_behavior: DisambiguationBehavior) -> ParseResult | WorkBenchParseFailure | None: if picklist.type == 0: # items from picklistitems table try: item = picklist.picklistitems.get(title=value) - return filter_and_upload({fieldname: item.value}, column) + return filter_and_upload({fieldname: item.value}, column, disambiguation_behavior) except ObjectDoesNotExist: if picklist.readonly: return WorkBenchParseFailure( @@ -144,11 +147,11 @@ def parse_with_picklist(picklist, fieldname: str, value: str, column: str) -> Pa column ) else: - return filter_and_upload({fieldname: value}, column)._replace( + return filter_and_upload({fieldname: value}, column, disambiguation_behavior)._replace( add_to_picklist=PicklistAddition( picklist=picklist, column=column, value=value) ) - return filter_and_upload({fieldname: value}) + return filter_and_upload({fieldname: value}, column, disambiguation_behavior) elif picklist.type == 1: # items from rows in some table # we ignore this type of picklist because it is primarily used to choose many-to-one's on forms diff --git a/specifyweb/backend/workbench/upload/scoping.py b/specifyweb/backend/workbench/upload/scoping.py index 24b7e887b11..f2d64ba7f34 100644 --- a/specifyweb/backend/workbench/upload/scoping.py +++ b/specifyweb/backend/workbench/upload/scoping.py @@ -147,6 +147,7 @@ def extend_columnoptions( matchBehavior=colopts.matchBehavior, nullAllowed=colopts.nullAllowed, default=colopts.default, + disambiguationBehavior=colopts.disambiguationBehavior, schemaitem=schemaitem, # Formatters are "scoped" here, that is, all they need is a value coming directly from the row. uiformatter=(None if scoped_formatter is None else CustomRepr(scoped_formatter, friendly_repr)), diff --git a/specifyweb/backend/workbench/upload/tests/testparsing.py b/specifyweb/backend/workbench/upload/tests/testparsing.py index 65867f44ea7..b68c50ae032 100644 --- a/specifyweb/backend/workbench/upload/tests/testparsing.py +++ b/specifyweb/backend/workbench/upload/tests/testparsing.py @@ -21,6 +21,7 @@ WorkBenchParseFailure from ..upload_results_schema import schema as upload_results_schema from ..upload_table import UploadTable +from specifyweb.specify import models from django.conf import settings @@ -444,7 +445,7 @@ def test_tree_cols_with_ignoreWhenBlank(self) -> None: ranks=dict( Genus=dict(name=parse_column_options('Genus')), Species=dict(name=parse_column_options('Species'), - author=ColumnOptions(column='Species Author', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None)) + author=ColumnOptions(column='Species Author', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None, disambiguationBehavior="ask")) ) ) data = [ @@ -466,7 +467,7 @@ def test_higher_tree_cols_with_ignoreWhenBlank(self) -> None: ranks=dict( Genus=dict(name=parse_column_options('Genus')), Species=dict(name=parse_column_options('Species'), - author=ColumnOptions(column='Species Author', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None)), + author=ColumnOptions(column='Species Author', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None, disambiguationBehavior="ask")), Subspecies=dict(name=parse_column_options('Subspecies')), ) ) @@ -488,7 +489,7 @@ def test_tree_cols_with_ignoreNever(self) -> None: ranks=dict( Genus=dict(name=parse_column_options('Genus')), Species=dict(name=parse_column_options('Species'), - author=ColumnOptions(column='Species Author', matchBehavior="ignoreNever", nullAllowed=True, default=None)) + author=ColumnOptions(column='Species Author', matchBehavior="ignoreNever", nullAllowed=True, default=None, disambiguationBehavior="ask")) ) ) data = [ @@ -508,7 +509,7 @@ def test_tree_cols_with_required(self) -> None: ranks=dict( Genus=dict(name=parse_column_options('Genus')), Species=dict(name=parse_column_options('Species'), - author=ColumnOptions(column='Species Author', matchBehavior="ignoreNever", nullAllowed=False, default=None)) + author=ColumnOptions(column='Species Author', matchBehavior="ignoreNever", nullAllowed=False, default=None, disambiguationBehavior="ask")) ) ) data = [ @@ -530,7 +531,7 @@ def test_tree_cols_with_ignoreAlways(self) -> None: ranks=dict( Genus=dict(name=parse_column_options('Genus')), Species=dict(name=parse_column_options('Species'), - author=ColumnOptions(column='Species Author', matchBehavior="ignoreAlways", nullAllowed=True, default=None)) + author=ColumnOptions(column='Species Author', matchBehavior="ignoreAlways", nullAllowed=True, default=None, disambiguationBehavior="ask")) ) ) data = [ @@ -551,7 +552,7 @@ def test_wbcols_with_ignoreWhenBlank(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=True, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -577,7 +578,7 @@ def test_wbcols_with_ignoreWhenBlank_and_default(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=True, default="John"), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=True, default="John", disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -609,7 +610,7 @@ def test_wbcols_with_ignoreNever(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -634,7 +635,7 @@ def test_wbcols_with_ignoreAlways(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreAlways", nullAllowed=True, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreAlways", nullAllowed=True, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -662,7 +663,7 @@ def test_wbcols_with_default(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default="John"), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default="John", disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -690,7 +691,7 @@ def test_wbcols_with_default_matching(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default="John"), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default="John", disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -721,7 +722,7 @@ def test_wbcols_with_default_and_null_disallowed(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default="John"), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default="John", disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -750,7 +751,7 @@ def test_wbcols_with_default_blank(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default=""), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default="", disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -780,7 +781,7 @@ def test_wbcols_with_null_disallowed(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=False, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -805,7 +806,7 @@ def test_wbcols_with_null_disallowed_and_ignoreWhenBlank(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=False, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreWhenBlank", nullAllowed=False, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -834,7 +835,7 @@ def test_wbcols_with_null_disallowed_and_ignoreAlways(self) -> None: name='Agent', wbcols={ 'lastname': parse_column_options('lastname'), - 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreAlways", nullAllowed=False, default=None), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreAlways", nullAllowed=False, default=None, disambiguationBehavior="ask"), }, overrideScope=None, static={}, @@ -857,3 +858,64 @@ def test_wbcols_with_null_disallowed_and_ignoreAlways(self) -> None: self.assertIsInstance(results[2].record_result, Uploaded) self.assertIsInstance(results[3].record_result, Matched) self.assertIsInstance(results[4].record_result, Matched) + +class DisambiguationBehaviorTests(UploadTestsBase): + def test_pickFirst_disambiguation_behavior(self) -> None: + # Upload some agents first + agent_plan = UploadTable( + name='Agent', + wbcols={ + 'firstname': parse_column_options('firstname'), + 'lastname': parse_column_options('lastname'), + 'email': parse_column_options('email'), + }, + overrideScope=None, + static={}, + toOne={}, + toMany={} + ) + agent_data = [ + {'lastname': 'Doe', 'firstname': 'John', 'email': '0'}, + {'lastname': 'Doe', 'firstname': 'John', 'email': '1'}, + ] + + results = do_upload(self.collection, agent_data, agent_plan, self.agent.id) + for result in results: + validate([result.to_json()], upload_results_schema, cls=Draft7Validator) + + self.assertIsInstance(results[0].record_result, Uploaded) + self.assertIsInstance(results[1].record_result, Uploaded) + + # Try to add some Collection Objects with ambiguous catalogers + plan = UploadTable( + name='Collectionobject', + wbcols={ + 'catalognumber': parse_column_options('Cat #'), + }, + overrideScope=None, + static={}, + toOne={ + 'cataloger': UploadTable( + name='Agent', + wbcols={ + 'lastname': parse_column_options('lastname'), + 'firstname': ColumnOptions(column='firstname', matchBehavior="ignoreNever", nullAllowed=True, default=None, disambiguationBehavior="pickFirst"), + }, + overrideScope=None, + static={}, + toOne={}, + toMany={}, + ) + }, + toMany={} + ) + data = [ + {'Cat #': '124', 'lastname': 'Doe', 'firstname': 'John'}, + {'Cat #': '125', 'lastname': 'Doe', 'firstname': 'Jane'} + ] + results = do_upload(self.collection, data, plan, self.agent.id) + for result in results: + validate([result.to_json()], upload_results_schema, cls=Draft7Validator) + + self.assertIsInstance(results[0].toOne['cataloger'].record_result, Matched, "Record was not disambiguated automatically despite having disambiguationBehavior='pickFirst'.") + self.assertIsInstance(results[1].toOne['cataloger'].record_result, Uploaded) diff --git a/specifyweb/backend/workbench/upload/upload_attachments.py b/specifyweb/backend/workbench/upload/upload_attachments.py index 410647cd0b0..b2d1527c5da 100644 --- a/specifyweb/backend/workbench/upload/upload_attachments.py +++ b/specifyweb/backend/workbench/upload/upload_attachments.py @@ -126,7 +126,8 @@ def add_attachments_to_plan( column=f"_ATTACHMENT_ORDINAL_{index}", matchBehavior="ignoreNever", nullAllowed=True, - default="0" + default="0", + disambiguationBehavior="ask" ) attackment_columns = {} for field in attachment_fields_to_copy: @@ -134,7 +135,8 @@ def add_attachments_to_plan( column=f"_ATTACHMENT_{field.upper()}_{index}", matchBehavior="ignoreNever", nullAllowed=True, - default=attachment_field_default(field) + default=attachment_field_default(field), + disambiguationBehavior="ask" ) attachment_uploadable = UploadTable( name="Attachment", diff --git a/specifyweb/backend/workbench/upload/upload_plan_schema.py b/specifyweb/backend/workbench/upload/upload_plan_schema.py index 3d35a7d0ae6..3cd7f748119 100644 --- a/specifyweb/backend/workbench/upload/upload_plan_schema.py +++ b/specifyweb/backend/workbench/upload/upload_plan_schema.py @@ -274,6 +274,12 @@ "default": None, "description": "When set use this value for any cells that are empty in this column.", }, + "disambiguationBehavior": { + "type": "string", + "enum": ["ask", "pickFirst"], + "default": "ask", + "description": "How to disambiguate when multiple records are matched.", + }, }, "required": ["column"], "additionalProperties": False, @@ -425,6 +431,7 @@ def parse_column_options(to_parse: str | dict) -> ColumnOptions: matchBehavior="ignoreNever", nullAllowed=True, default=None, + disambiguationBehavior="ask", ) else: return ColumnOptions( @@ -432,4 +439,5 @@ def parse_column_options(to_parse: str | dict) -> ColumnOptions: matchBehavior=to_parse.get("matchBehavior", "ignoreNever"), nullAllowed=to_parse.get("nullAllowed", True), default=to_parse.get("default", None), + disambiguationBehavior=to_parse.get("disambiguationBehavior", "ask") ) diff --git a/specifyweb/backend/workbench/upload/upload_table.py b/specifyweb/backend/workbench/upload/upload_table.py index a5ab4cb3f9a..f0cc413ef80 100644 --- a/specifyweb/backend/workbench/upload/upload_table.py +++ b/specifyweb/backend/workbench/upload/upload_table.py @@ -674,12 +674,21 @@ def _match( n_matched = len(ids) if n_matched > 1: + if self._disambiguation_pick_first(): + return Matched(id=ids[0], info=info) return MatchedMultiple(ids=ids, key=repr(cache_key), info=info) elif n_matched == 1: return Matched(id=ids[0], info=info) else: return None + def _disambiguation_pick_first(self) -> bool: + """Disambiguate by picking the first record if any field uses 'pickFirst'.""" + for p in self.parsedFields: + if p.filter_on and p.disambiguation_behavior == "pickFirst": + return True + return False + def _check_missing_required(self) -> ParseFailures | None: missing_requireds = [ # TODO: there should probably be a different structure for diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx index 6b7bb023457..c717e86a94b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx @@ -651,6 +651,12 @@ export function Mapper(props: { line, defaultValue, }), + onChangeDisambiguationBehavior: (disambiguationBehavior) => + dispatch({ + type: 'ChangeDisambiguationBehaviorAction', + line, + disambiguationBehavior, + }), }), previewOption: { optionName: 'mappingOptions', diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/MapperComponents.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/MapperComponents.tsx index f8465cea9fa..ff9a781b052 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/MapperComponents.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/MapperComponents.tsx @@ -32,7 +32,7 @@ import { } from './Mapper'; import { getMappingLineData } from './navigator'; import { navigatorSpecs } from './navigatorSpecs'; -import type { ColumnOptions, MatchBehaviors } from './uploadPlanParser'; +import type { ColumnOptions, DisambiguationBehaviors, MatchBehaviors } from './uploadPlanParser'; export function MappingsControlPanel({ showHiddenFields, @@ -229,6 +229,7 @@ export function mappingOptionsMenu({ onChangeMatchBehaviour: handleChangeMatchBehaviour, onToggleAllowNulls: handleToggleAllowNulls, onChangeDefaultValue: handleChangeDefaultValue, + onChangeDisambiguationBehavior: handleChangeDisambiguationBehavior, }: { readonly id: (suffix: string) => string; readonly isReadOnly: boolean; @@ -236,6 +237,7 @@ export function mappingOptionsMenu({ readonly onChangeMatchBehaviour: (matchBehavior: MatchBehaviors) => void; readonly onToggleAllowNulls: (allowNull: boolean) => void; readonly onChangeDefaultValue: (defaultValue: string | null) => void; + readonly onChangeDisambiguationBehavior: (disambiguationBehavior: DisambiguationBehaviors) => void; }): IR { return { matchBehavior: { @@ -318,6 +320,38 @@ export function mappingOptionsMenu({ ), title: wbPlanText.defaultValueDescription(), }, + disambiguationBehavior: { + optionLabel: ( + <> + {wbPlanText.disambiguationBehavior()} + + + ), + }, }; } diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts index 8d804e02644..c0c4bfec10e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts @@ -47,6 +47,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, /* @@ -57,6 +58,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -66,6 +68,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -75,6 +78,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -84,6 +88,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -93,6 +98,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -102,6 +108,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -111,6 +118,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -120,6 +128,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -129,6 +138,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, */ @@ -161,6 +171,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -170,6 +181,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -179,6 +191,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -188,6 +201,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -197,6 +211,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -206,6 +221,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -215,6 +231,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -224,6 +241,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -233,6 +251,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -242,6 +261,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, ], diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/linesGetter.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/linesGetter.ts index 41ff98b9aa2..3ad19a5752a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/linesGetter.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/linesGetter.ts @@ -21,6 +21,7 @@ export const defaultColumnOptions: ColumnOptions = { matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', } as const; export const columnOptionsAreDefault = ( diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts index c59e2bccd55..a411082125a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts @@ -27,8 +27,8 @@ import type { MappingState, SelectElementPosition, } from './Mapper'; -import { emptyMapping } from './mappingHelpers'; -import type { MatchBehaviors } from './uploadPlanParser'; +import { emptyMapping, mappingPathToString } from './mappingHelpers'; +import type { DisambiguationBehaviors, MatchBehaviors } from './uploadPlanParser'; const modifyLine = ( state: MappingState, @@ -152,6 +152,14 @@ type ChangeDefaultValueAction = Action< } >; +type ChangeDisambiguationBehaviorAction = Action< + 'ChangeDisambiguationBehaviorAction', + { + readonly line: number; + readonly disambiguationBehavior: DisambiguationBehaviors; + } +>; + type UpdateLinesAction = Action< 'UpdateLinesAction', { readonly lines: RA } @@ -178,6 +186,7 @@ export type MappingActions = | ChangeBatchEditPrefs | ChangeDefaultValueAction | ChangeMatchBehaviorAction + | ChangeDisambiguationBehaviorAction | ChangeSelectElementValueAction | ChangMustMatchPrefAction | ClearMappingLineAction @@ -393,6 +402,23 @@ export const reducer = generateReducer({ }), changesMade: true, }), + ChangeDisambiguationBehaviorAction: ({ state, action }) => { + const mappingPath = state.lines[action.line].mappingPath; + return { + ...state, + lines: state.lines.map((line) => + isSameMappingPath(line.mappingPath, mappingPath) ? + { + ...line, + columnOptions: { + ...line.columnOptions, + disambiguationBehavior: action.disambiguationBehavior, + }, + } : line + ), + changesMade: true, + } + }, UpdateLinesAction: ({ state, action: { lines } }) => ({ ...state, lines, @@ -418,3 +444,6 @@ export const reducer = generateReducer({ batchEditPrefs: action.prefs, }), }); + +const isSameMappingPath = (pathA: MappingPath, pathB: MappingPath): boolean => + mappingPathToString(pathA.slice(0, -1)) === mappingPathToString(pathB.slice(0, -1)); \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts index 21323955077..61528d9ae81 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts @@ -13,11 +13,13 @@ import { formatToManyIndex, formatTreeRank } from './mappingHelpers'; import { RANK_KEY_DELIMITER } from './uploadPlanBuilder'; export type MatchBehaviors = 'ignoreAlways' | 'ignoreNever' | 'ignoreWhenBlank'; +export type DisambiguationBehaviors = 'ask' | 'pickFirst' export type ColumnOptions = { readonly matchBehavior: MatchBehaviors; readonly nullAllowed: boolean; readonly default: string | null; + readonly disambiguationBehavior: DisambiguationBehaviors; }; export type ColumnDefinition = diff --git a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts index 31f57f98b8c..975e90a8d9d 100644 --- a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts +++ b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts @@ -770,4 +770,19 @@ export const wbPlanText = createDictionary({ 'O conjunto de dados selecionado não possui um plano de upload. Selecione outro.', 'hr-hr': 'Odabrani skup podataka nema plan prijenosa. Odaberite drugi.', }, + disambiguationBehavior: { + 'en-us': 'Disambiguation Behavior:', + }, + ask: { + 'en-us': 'Ask', + }, + askDescription: { + 'en-us': 'You will be prompted to pick a record out of all the records matched to this field.', + }, + pickFirst: { + 'en-us': 'Pick first', + }, + pickFirstDescription: { + 'en-us': 'When multiple records are matched to this field, the first record will be picked automatically.', + } } as const); diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/mappinglines.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/mappinglines.1.json index 76d3c75cd21..9b1f292c3ba 100644 --- a/specifyweb/frontend/js_src/lib/tests/fixtures/mappinglines.1.json +++ b/specifyweb/frontend/js_src/lib/tests/fixtures/mappinglines.1.json @@ -7,7 +7,8 @@ "columnOptions": { "matchBehavior": "ignoreWhenBlank", "nullAllowed": true, - "default": "qwerty" + "default": "qwerty", + "disambiguationBehavior": "ask" } }, { @@ -16,7 +17,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -25,7 +27,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -34,7 +37,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -43,7 +47,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -52,7 +57,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -61,7 +67,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -70,7 +77,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -79,7 +87,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -88,7 +97,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -97,7 +107,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -106,7 +117,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -121,7 +133,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -136,7 +149,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -151,7 +165,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -160,7 +175,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -175,7 +191,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -190,7 +207,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -205,7 +223,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -214,7 +233,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -229,7 +249,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -244,7 +265,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -259,7 +281,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -268,7 +291,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -283,7 +307,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -298,7 +323,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -313,7 +339,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -322,7 +349,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -337,7 +365,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -352,7 +381,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -367,7 +397,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -376,7 +407,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -391,7 +423,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -400,7 +433,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -409,7 +443,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -418,7 +453,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -427,7 +463,8 @@ "columnOptions": { "matchBehavior": "ignoreAlways", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -436,7 +473,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -445,7 +483,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -454,7 +493,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -463,7 +503,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -472,7 +513,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -481,7 +523,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -490,7 +533,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -499,7 +543,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -508,7 +553,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -517,7 +563,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -526,7 +573,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -535,7 +583,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -544,7 +593,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -553,7 +603,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } } ], diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json index ea0336746b4..58caf26d7db 100644 --- a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json +++ b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json @@ -87,6 +87,7 @@ "matchBehavior": "ignoreWhenBlank", "nullAllowed": true, "default": "qwerty", + "disambiguationBehavior": "ask", "column": "BMSM No." } }, @@ -286,6 +287,7 @@ "matchBehavior": "ignoreAlways", "nullAllowed": true, "default": null, + "disambiguationBehavior": "ask", "column": "Class" } } diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/wbplanviewlines.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/wbplanviewlines.1.json index e7149c8bb8d..5e13ff91de5 100644 --- a/specifyweb/frontend/js_src/lib/tests/fixtures/wbplanviewlines.1.json +++ b/specifyweb/frontend/js_src/lib/tests/fixtures/wbplanviewlines.1.json @@ -7,7 +7,8 @@ "columnOptions": { "matchBehavior": "ignoreWhenBlank", "nullAllowed": true, - "default": "qwerty" + "default": "qwerty", + "disambiguationBehavior": "ask" } }, { @@ -16,7 +17,8 @@ "columnOptions": { "matchBehavior": "ignoreAlways", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -25,7 +27,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -34,7 +37,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -43,7 +47,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -52,7 +57,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -61,7 +67,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -70,7 +77,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -79,7 +87,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -88,7 +97,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -97,7 +107,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -106,7 +117,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -115,7 +127,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -124,7 +137,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -133,7 +147,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -142,7 +157,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -151,7 +167,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -160,7 +177,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -175,7 +193,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -190,7 +209,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -199,7 +219,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -208,7 +229,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -217,7 +239,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -232,7 +255,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -241,7 +265,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -250,7 +275,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -259,7 +285,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -268,7 +295,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -283,7 +311,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -292,7 +321,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -301,7 +331,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -310,7 +341,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -319,7 +351,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -328,7 +361,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -337,7 +371,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -346,7 +381,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -355,7 +391,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -364,7 +401,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -373,7 +411,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -382,7 +421,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -391,7 +431,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -400,7 +441,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -409,7 +451,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -418,7 +461,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -427,7 +471,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -436,7 +481,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -445,7 +491,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -454,7 +501,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -463,7 +511,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -472,7 +521,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -481,7 +531,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -490,7 +541,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -499,7 +551,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -508,7 +561,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -517,7 +571,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -526,7 +581,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -535,7 +591,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -544,7 +601,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -553,7 +611,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -562,7 +621,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -571,7 +631,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -580,7 +641,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -595,7 +657,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -610,7 +673,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -625,7 +689,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -634,7 +699,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -649,7 +715,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -664,7 +731,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -679,7 +747,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -688,7 +757,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -703,7 +773,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -718,7 +789,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -733,7 +805,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -742,7 +815,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -757,7 +831,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -772,7 +847,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } }, { @@ -787,7 +863,8 @@ "columnOptions": { "matchBehavior": "ignoreNever", "nullAllowed": true, - "default": null + "default": null, + "disambiguationBehavior": "ask" } } ],