From 3826ca88b8446404420c47c739be5426fd6d7a60 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 4 May 2026 09:49:57 -0500 Subject: [PATCH 01/12] WIP add disambiguationBehavior option --- .../workbench/upload/column_options.py | 3 ++ .../backend/workbench/upload/scoping.py | 1 + .../workbench/upload/upload_attachments.py | 3 +- .../workbench/upload/upload_plan_schema.py | 8 +++++ .../lib/components/WbPlanView/Mapper.tsx | 6 ++++ .../WbPlanView/MapperComponents.tsx | 36 ++++++++++++++++++- .../lib/components/WbPlanView/linesGetter.ts | 1 + .../components/WbPlanView/mappingReducer.ts | 20 +++++++++++ .../components/WbPlanView/uploadPlanParser.ts | 2 ++ .../js_src/lib/localization/wbPlan.ts | 15 ++++++++ 10 files changed, 93 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/workbench/upload/column_options.py b/specifyweb/backend/workbench/upload/column_options.py index 4a9bd278387..1094e660ac5 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,6 +15,7 @@ class ColumnOptions(NamedTuple): matchBehavior: MatchBehavior nullAllowed: bool default: str | None + disambiguationBehavior: DisambiguationBehavior = "ask" # Default value is temporary while I update all the code def to_json(self) -> dict | str: if self.matchBehavior == "ignoreNever" and self.nullAllowed and self.default is None: @@ -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/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/upload_attachments.py b/specifyweb/backend/workbench/upload/upload_attachments.py index 410647cd0b0..98ff0afe4d4 100644 --- a/specifyweb/backend/workbench/upload/upload_attachments.py +++ b/specifyweb/backend/workbench/upload/upload_attachments.py @@ -134,7 +134,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..35e5d5ad152 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": "ignoreNever", + "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/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/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..cdaa079bc30 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts @@ -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,17 @@ export const reducer = generateReducer({ }), changesMade: true, }), + ChangeDisambiguationBehaviorAction: ({ state, action }) => ({ + ...state, + lines: modifyLine(state, action.line, { + ...state.lines[action.line], + columnOptions: { + ...state.lines[action.line].columnOptions, + disambiguationBehavior: action.disambiguationBehavior, + }, + }), + changesMade: true, + }), UpdateLinesAction: ({ state, action: { lines } }) => ({ ...state, lines, 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); From 7b882994cc0ab52d3fc7758c7393937e98d6fc8a Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 4 May 2026 15:51:25 -0500 Subject: [PATCH 02/12] Add pickfirst preference to workbench validation Co-authored-by: Copilot --- .../workbench/upload/column_options.py | 2 +- .../backend/workbench/upload/parsing.py | 29 ++++++++++--------- .../backend/workbench/upload/upload_table.py | 9 ++++++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/specifyweb/backend/workbench/upload/column_options.py b/specifyweb/backend/workbench/upload/column_options.py index 1094e660ac5..1612097ec55 100644 --- a/specifyweb/backend/workbench/upload/column_options.py +++ b/specifyweb/backend/workbench/upload/column_options.py @@ -28,7 +28,7 @@ class ExtendedColumnOptions(NamedTuple): matchBehavior: MatchBehavior nullAllowed: bool default: str | None - DisambiguationBehavior: DisambiguationBehavior + 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..634d238c101 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 + disambiguationBehavior: 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, disambiguationBehavior: DisambiguationBehavior): + return cls(filter_on=filter_on, upload=ps.payload, add_to_picklist=add_to_picklist, column=column, + missing_required=missing_required, disambiguationBehavior=disambiguationBehavior) 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) -> 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/upload_table.py b/specifyweb/backend/workbench/upload/upload_table.py index a5ab4cb3f9a..855a0226f22 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.disambiguationBehavior == "pickFirst": + return True + return False + def _check_missing_required(self) -> ParseFailures | None: missing_requireds = [ # TODO: there should probably be a different structure for From 9ce2c151b297959f0f5f740d23fc9ae05154b7cf Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 4 May 2026 16:28:56 -0500 Subject: [PATCH 03/12] Change setting for all fields in the same table --- .../components/WbPlanView/mappingReducer.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts index cdaa079bc30..fa1ecdccc8e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts @@ -27,7 +27,7 @@ import type { MappingState, SelectElementPosition, } from './Mapper'; -import { emptyMapping } from './mappingHelpers'; +import { emptyMapping, mappingPathToString } from './mappingHelpers'; import type { MatchBehaviors } from './uploadPlanParser'; const modifyLine = ( @@ -402,17 +402,23 @@ export const reducer = generateReducer({ }), changesMade: true, }), - ChangeDisambiguationBehaviorAction: ({ state, action }) => ({ - ...state, - lines: modifyLine(state, action.line, { - ...state.lines[action.line], - columnOptions: { - ...state.lines[action.line].columnOptions, - disambiguationBehavior: action.disambiguationBehavior, - }, - }), - 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: { + ...state.lines[action.line].columnOptions, + disambiguationBehavior: action.disambiguationBehavior, + }, + } : line + ), + changesMade: true, + } + }, UpdateLinesAction: ({ state, action: { lines } }) => ({ ...state, lines, @@ -438,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 From f3de469e5e241e848abb4bc94d7a7dfc7f3ab649 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 6 May 2026 09:11:21 -0500 Subject: [PATCH 04/12] Fix import --- .../frontend/js_src/lib/components/WbPlanView/mappingReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts index fa1ecdccc8e..1b9312cf077 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts @@ -28,7 +28,7 @@ import type { SelectElementPosition, } from './Mapper'; import { emptyMapping, mappingPathToString } from './mappingHelpers'; -import type { MatchBehaviors } from './uploadPlanParser'; +import type { DisambiguationBehaviors, MatchBehaviors } from './uploadPlanParser'; const modifyLine = ( state: MappingState, From e0bb7baaf5f83eb94be5ccd73fc601d9b263a876 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 6 May 2026 09:44:18 -0500 Subject: [PATCH 05/12] Update tests Co-authored-by: Copilot --- .../workbench/upload/column_options.py | 2 +- .../workbench/upload/tests/testparsing.py | 73 +++++++++++++++---- .../workbench/upload/upload_attachments.py | 3 +- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/specifyweb/backend/workbench/upload/column_options.py b/specifyweb/backend/workbench/upload/column_options.py index 1612097ec55..2d628d64b77 100644 --- a/specifyweb/backend/workbench/upload/column_options.py +++ b/specifyweb/backend/workbench/upload/column_options.py @@ -15,7 +15,7 @@ class ColumnOptions(NamedTuple): matchBehavior: MatchBehavior nullAllowed: bool default: str | None - disambiguationBehavior: DisambiguationBehavior = "ask" # Default value is temporary while I update all the code + disambiguationBehavior: DisambiguationBehavior def to_json(self) -> dict | str: if self.matchBehavior == "ignoreNever" and self.nullAllowed and self.default is None: diff --git a/specifyweb/backend/workbench/upload/tests/testparsing.py b/specifyweb/backend/workbench/upload/tests/testparsing.py index 65867f44ea7..7090ed951fb 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,43 @@ 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: + 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 #': '123', 'lastname': 'Doe', 'firstname': 'John'}, + {'Cat #': '123', 'lastname': 'Doe', 'firstname': 'Jane'} + ] + + models.Agent.objects.create(firstname='John', lastname='Doe', agenttype=0) + models.Agent.objects.create(firstname='John', lastname='Doe', agenttype=0) + models.Agent.objects.create(firstname='Jack', lastname='Doe', agenttype=0) + + 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].record_result, Matched, "Record was not disambiguated automatically despite having disambiguationBehavior='pickFirst'.") + self.assertIsInstance(results[1].record_result, Uploaded) diff --git a/specifyweb/backend/workbench/upload/upload_attachments.py b/specifyweb/backend/workbench/upload/upload_attachments.py index 98ff0afe4d4..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: From 47ef328f5f32509c537091abb3a956abfdbaee9a Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 08:44:08 -0500 Subject: [PATCH 06/12] Update tests with disambiguationBehavior Co-authored-by: Copilot --- .../backend/workbench/upload/parsing.py | 2 +- .../WbPlanView/__tests__/linesGetter.test.ts | 11 + .../lib/tests/fixtures/wbplanviewlines.1.json | 231 ++++++++++++------ 3 files changed, 166 insertions(+), 78 deletions(-) diff --git a/specifyweb/backend/workbench/upload/parsing.py b/specifyweb/backend/workbench/upload/parsing.py index 634d238c101..4c02d7023fc 100644 --- a/specifyweb/backend/workbench/upload/parsing.py +++ b/specifyweb/backend/workbench/upload/parsing.py @@ -50,7 +50,7 @@ def match_key(self) -> str: return filter_match_key(self.filter_on) -def filter_and_upload(f: Filter, column: str, disambiguation_behavior: DisambiguationBehavior) -> ParseResult: +def filter_and_upload(f: Filter, column: str, disambiguation_behavior: DisambiguationBehavior = 'ask') -> ParseResult: return ParseResult(f, f, None, column, None, disambiguation_behavior) 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..d88a1ba537a 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', }, }, /* @@ -161,6 +162,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -170,6 +172,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -179,6 +182,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -188,6 +192,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -197,6 +202,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -206,6 +212,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -215,6 +222,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -224,6 +232,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -233,6 +242,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, { @@ -242,6 +252,7 @@ theories(getLinesFromHeaders, [ matchBehavior: 'ignoreNever', nullAllowed: true, default: null, + disambiguationBehavior: 'ask', }, }, ], 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" } } ], From 8d720c6142b98bd6c30f851b079c69a29af46ef1 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 08:45:34 -0500 Subject: [PATCH 07/12] Update commented tests --- .../components/WbPlanView/__tests__/linesGetter.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) 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 d88a1ba537a..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 @@ -58,6 +58,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -67,6 +68,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -76,6 +78,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -85,6 +88,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -94,6 +98,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -103,6 +108,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -112,6 +118,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -121,6 +128,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, * { @@ -130,6 +138,7 @@ theories(getLinesFromHeaders, [ * matchBehavior: 'ignoreNever', * nullAllowed: true, * default: null, + * disambiguationBehavior: 'ask, * }, * }, */ From 7c59a1194bbf06e97096afcbc10df1d522d3f5d7 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 09:46:33 -0500 Subject: [PATCH 08/12] Update mapping tests --- .../lib/tests/fixtures/mappinglines.1.json | 153 ++++++++++++------ .../lib/tests/fixtures/uploadplan.1.json | 2 + 2 files changed, 104 insertions(+), 51 deletions(-) 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" } } From 30a586af237fcc75989f7a1369779d1c97cd748e Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 14:08:07 -0500 Subject: [PATCH 09/12] Fix setting disambiguationBehavior overwriting other options --- specifyweb/backend/workbench/upload/upload_plan_schema.py | 2 +- .../frontend/js_src/lib/components/WbPlanView/mappingReducer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/workbench/upload/upload_plan_schema.py b/specifyweb/backend/workbench/upload/upload_plan_schema.py index 35e5d5ad152..3cd7f748119 100644 --- a/specifyweb/backend/workbench/upload/upload_plan_schema.py +++ b/specifyweb/backend/workbench/upload/upload_plan_schema.py @@ -277,7 +277,7 @@ "disambiguationBehavior": { "type": "string", "enum": ["ask", "pickFirst"], - "default": "ignoreNever", + "default": "ask", "description": "How to disambiguate when multiple records are matched.", }, }, diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts index 1b9312cf077..a411082125a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingReducer.ts @@ -411,7 +411,7 @@ export const reducer = generateReducer({ { ...line, columnOptions: { - ...state.lines[action.line].columnOptions, + ...line.columnOptions, disambiguationBehavior: action.disambiguationBehavior, }, } : line From 5896785cc758155acf4427c9ba6c1dd0fa7a084e Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 15:08:03 -0500 Subject: [PATCH 10/12] Fix test not actually triggering disambiguation --- .../workbench/upload/tests/testparsing.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/specifyweb/backend/workbench/upload/tests/testparsing.py b/specifyweb/backend/workbench/upload/tests/testparsing.py index 7090ed951fb..b68c50ae032 100644 --- a/specifyweb/backend/workbench/upload/tests/testparsing.py +++ b/specifyweb/backend/workbench/upload/tests/testparsing.py @@ -861,6 +861,32 @@ def test_wbcols_with_null_disallowed_and_ignoreAlways(self) -> None: 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={ @@ -884,17 +910,12 @@ def test_pickFirst_disambiguation_behavior(self) -> None: toMany={} ) data = [ - {'Cat #': '123', 'lastname': 'Doe', 'firstname': 'John'}, - {'Cat #': '123', 'lastname': 'Doe', 'firstname': 'Jane'} + {'Cat #': '124', 'lastname': 'Doe', 'firstname': 'John'}, + {'Cat #': '125', 'lastname': 'Doe', 'firstname': 'Jane'} ] - - models.Agent.objects.create(firstname='John', lastname='Doe', agenttype=0) - models.Agent.objects.create(firstname='John', lastname='Doe', agenttype=0) - models.Agent.objects.create(firstname='Jack', lastname='Doe', agenttype=0) - 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].record_result, Matched, "Record was not disambiguated automatically despite having disambiguationBehavior='pickFirst'.") - self.assertIsInstance(results[1].record_result, Uploaded) + + 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) From 7df9e83fcd4bb5082c736a354961bef21caccd65 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 15:43:08 -0500 Subject: [PATCH 11/12] Update disambiguationBehavior to disambiguation_behavior --- specifyweb/backend/workbench/upload/parsing.py | 6 +++--- specifyweb/backend/workbench/upload/upload_table.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/backend/workbench/upload/parsing.py b/specifyweb/backend/workbench/upload/parsing.py index 4c02d7023fc..7abcad0b1e3 100644 --- a/specifyweb/backend/workbench/upload/parsing.py +++ b/specifyweb/backend/workbench/upload/parsing.py @@ -38,13 +38,13 @@ class ParseResult(NamedTuple): add_to_picklist: PicklistAddition | None column: str missing_required: str | None - disambiguationBehavior: DisambiguationBehavior + 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, disambiguationBehavior: DisambiguationBehavior): + 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, disambiguationBehavior=disambiguationBehavior) + missing_required=missing_required, disambiguation_behavior=disambiguation_behavior) def match_key(self) -> str: return filter_match_key(self.filter_on) diff --git a/specifyweb/backend/workbench/upload/upload_table.py b/specifyweb/backend/workbench/upload/upload_table.py index 855a0226f22..f0cc413ef80 100644 --- a/specifyweb/backend/workbench/upload/upload_table.py +++ b/specifyweb/backend/workbench/upload/upload_table.py @@ -685,7 +685,7 @@ def _match( 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.disambiguationBehavior == "pickFirst": + if p.filter_on and p.disambiguation_behavior == "pickFirst": return True return False From f3fda4ba83ddd353cc4de5528f736a34d720159b Mon Sep 17 00:00:00 2001 From: alesan99 Date: Fri, 8 May 2026 15:49:14 -0500 Subject: [PATCH 12/12] Fix test_column_options_to_json test --- specifyweb/backend/workbench/upload/column_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/backend/workbench/upload/column_options.py b/specifyweb/backend/workbench/upload/column_options.py index 2d628d64b77..95b8f3fbb29 100644 --- a/specifyweb/backend/workbench/upload/column_options.py +++ b/specifyweb/backend/workbench/upload/column_options.py @@ -18,7 +18,7 @@ class ColumnOptions(NamedTuple): 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())