Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import unittest
from jsonschema import validate, Draft7Validator # type: ignore

from specifyweb.backend.businessrules.exceptions import BusinessRuleException

from ..upload_result import *
from ..upload_results_schema import schema

Expand Down Expand Up @@ -36,6 +38,37 @@ def testFailedBusinessRule(self, failedBusinessRule: FailedBusinessRule):
j = json.dumps(failedBusinessRule.to_json())
self.assertEqual(failedBusinessRule, FailedBusinessRule.from_json(json.loads(j)))

def testBusinessRuleExceptionPayload(self):
info = ReportInfo(
tableName="Collectionobject",
columns=["catalogNumber"],
treeInfo=None,
)
payload = {
"localizationKey": "childFieldNotUnique",
"table": "Collectionobject",
"fieldName": "catalognumber",
"fieldData": {"catalognumber": "0037481"},
"parentField": "collection",
"parentData": {"collection": "Collection object (360449)"},
"conflicting": [3347460],
}

self.assertEqual(
to_failed_business_rule(
BusinessRuleException(
"Collectionobject must have unique catalognumber in collection",
payload,
),
info,
),
FailedBusinessRule(
"Collectionobject must have unique catalognumber in collection",
payload,
info,
),
)

@given(noMatch=infer)
def testNoMatch(self, noMatch: NoMatch):
j = json.dumps(noMatch.to_json())
Expand Down
3 changes: 2 additions & 1 deletion specifyweb/backend/workbench/upload/treerecord.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
FailedBusinessRule,
ReportInfo,
TreeInfo,
to_failed_business_rule,
)
from .uploadable import (
Row,
Expand Down Expand Up @@ -954,7 +955,7 @@ def _upload(
obj = self._do_insert(model, **new_attrs)
except (BusinessRuleException, IntegrityError) as e:
return UploadResult(
FailedBusinessRule(str(e), {}, info), parent_result, {}
to_failed_business_rule(e, info), parent_result, {}
)

result = UploadResult(Uploaded(obj.id, info, []), parent_result, {})
Expand Down
26 changes: 25 additions & 1 deletion specifyweb/backend/workbench/upload/upload_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

from typing import Literal

from specifyweb.backend.businessrules.exceptions import BusinessRuleException

from .parsing import WorkBenchParseFailure

Failure = Literal["Failure"]
BusinessRulePayloadValue = (
str
| int
| bool
| None
| list[str]
| list[int]
| dict[str, str | int | bool | None]
)
BusinessRulePayload = dict[str, BusinessRulePayloadValue]


class TreeInfo(NamedTuple):
Expand Down Expand Up @@ -215,7 +227,7 @@ def from_json(json: dict) -> "Deleted":

class FailedBusinessRule(NamedTuple):
message: str
payload: dict[str, str | int | list[str] | list[int]]
payload: BusinessRulePayload
info: ReportInfo

def get_id(self) -> Failure:
Expand All @@ -238,6 +250,18 @@ def from_json(json: dict) -> "FailedBusinessRule":
)


def to_failed_business_rule(exception: Exception, info: ReportInfo) -> FailedBusinessRule:
if (
isinstance(exception, BusinessRuleException)
and len(exception.args) >= 2
and isinstance(exception.args[0], str)
and isinstance(exception.args[1], dict)
):
return FailedBusinessRule(exception.args[0], exception.args[1], info)

return FailedBusinessRule(str(exception), {}, info)


class NoMatch(NamedTuple):
info: ReportInfo

Expand Down
7 changes: 4 additions & 3 deletions specifyweb/backend/workbench/upload/upload_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
PicklistAddition,
ParseFailures,
PropagatedFailure,
to_failed_business_rule,
)
from .uploadable import (
NULL_RECORD,
Expand Down Expand Up @@ -760,7 +761,7 @@ def _do_upload(
picklist_additions = self._do_picklist_additions()
except (BusinessRuleException, IntegrityError) as e:
return UploadResult(
FailedBusinessRule(str(e), {}, info), to_one_results, {}
to_failed_business_rule(e, info), to_one_results, {}
)

record = Uploaded(uploaded.id, info, picklist_additions)
Expand Down Expand Up @@ -865,7 +866,7 @@ def delete_row(self, parent_obj=None) -> UploadResult:
reference_record.delete()
result = Deleted(self.current_id, info)
except (BusinessRuleException, IntegrityError) as e:
result = FailedBusinessRule(str(e), {}, info)
result = to_failed_business_rule(e, info)

to_one_deleted: dict[str, UploadResult] = {
key: value.delete_row()
Expand Down Expand Up @@ -1066,7 +1067,7 @@ def _do_upload(
picklist_additions = self._do_picklist_additions()
except (BusinessRuleException, IntegrityError) as e:
return UploadResult(
FailedBusinessRule(str(e), {}, info), to_one_results, {}
to_failed_business_rule(e, info), to_one_results, {}
)

record: Updated | NoChange = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,57 @@ export function resolveBackendParsingMessage(
else return undefined;
}

function withConflictingRecordIds(
message: LocalizedString,
payload: IR<unknown>
): LocalizedString {
const conflicting = payload.conflicting;
return Array.isArray(conflicting) && conflicting.length > 0
? localized(
`${message} (Conflicting record IDs: ${conflicting.join(', ')})`
)
: message;
}

function getStringPayload(payload: IR<unknown>, key: string): string {
const value = payload[key];
return typeof value === 'string' ? value : '';
}

function resolveBackendBusinessRuleMessage(
payload: IR<unknown>
): LocalizedString | undefined {
if (payload.localizationKey === 'fieldNotUnique')
return withConflictingRecordIds(
backEndText.fieldNotUnique({
tableName: getStringPayload(payload, 'table'),
fieldName: getStringPayload(payload, 'fieldName'),
}),
payload
);
else if (payload.localizationKey === 'childFieldNotUnique')
return withConflictingRecordIds(
backEndText.childFieldNotUnique({
tableName: getStringPayload(payload, 'table'),
fieldName: getStringPayload(payload, 'fieldName'),
parentField: getStringPayload(payload, 'parentField'),
}),
payload
);
else return undefined;
}

/** Back-end sends a validation key. Front-end translates it */
export function resolveValidationMessage(
key: string,
payload: IR<unknown>
): LocalizedString {
const baseParsedMessage = resolveBackendParsingMessage(key, payload);
const businessRuleMessage = resolveBackendBusinessRuleMessage(payload);
if (baseParsedMessage !== undefined) {
return baseParsedMessage;
} else if (businessRuleMessage !== undefined) {
return businessRuleMessage;
} else if (key === 'failedParsingPickList')
return backEndText.failedParsingPickList({
value: `"${payload.value as string}"`,
Expand Down
Loading