Skip to content

Commit 741a5e3

Browse files
authored
Merge pull request #2702 from specify/issue-2668
Improve error response from backend
2 parents ea61870 + 0387638 commit 741a5e3

28 files changed

+695
-87
lines changed

specifyweb/businessrules/accessionagent_rules.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
@orm_signal_handler('pre_save', 'Accessionagent')
66
def agent_division_must_not_be_null(accessionagent):
77
if accessionagent.agent_id is None:
8-
raise BusinessRuleException(_("AccessionAgent -> Agent relationship is required."))
8+
raise BusinessRuleException(
9+
_("AccessionAgent -> Agent relationship is required."),
10+
{"table" : "Accessionagent",
11+
"fieldName" : "agent" })

specifyweb/businessrules/agent_rules.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,37 @@
55
@orm_signal_handler('pre_delete', 'Agent')
66
def agent_delete_blocked_by_related_specifyuser(agent):
77
try:
8-
Specifyuser.objects.get(agents=agent)
8+
user = Specifyuser.objects.get(agents=agent)
99
except Specifyuser.DoesNotExist:
1010
return
11-
raise BusinessRuleException("agent cannot be deleted while associated with a specifyuser")
11+
raise BusinessRuleException(
12+
"agent cannot be deleted while associated with a specifyuser",
13+
{"table" : "Agent",
14+
"fieldName" : "specifyuser",
15+
"agentid" : agent.id,
16+
"specifyuserid": user.id})
1217

1318
# Disabling this rule because system agents must be created separate from divisions
1419
# @orm_signal_handler('pre_save', 'Agent')
1520
# def agent_division_must_not_be_null(agent):
1621
# if agent.division is None:
17-
# raise BusinessRuleException("agent.division cannot be null")
22+
# raise BusinessRuleException(
23+
# "agent.division cannot be null",
24+
# {"table" : "Agent",
25+
# "fieldName" : "division",
26+
# "agentid" : agent.id})
1827

1928
@orm_signal_handler('pre_save', 'Agent')
2029
def agent_types_other_and_group_do_not_have_addresses(agent):
2130
from specifyweb.specify.agent_types import agent_types
2231
if agent.agenttype is None:
23-
raise BusinessRuleException("agenttype cannot be null")
24-
if agent_types[agent.agenttype] in ('Other', 'Group'):
25-
agent.addresses.all().delete()
32+
raise BusinessRuleException(
33+
"agenttype cannot be null",
34+
{"table" : "Agent",
35+
"fieldName" : "agenttype",
36+
"agentid" : agent.id})
37+
38+
# This Business Rule (Agents of type Other/Group can not have Addresses) was removed
39+
# See https://github.com/specify/specify7/issues/2518 for more information
40+
# if agent_types[agent.agenttype] in ('Other', 'Group'):
41+
# agent.addresses.all().delete()

specifyweb/businessrules/determination_rules.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def determination_pre_save(det):
1616
while acceptedtaxon_id is not None:
1717
if acceptedtaxon_id == taxon_id: break
1818
limit -= 1
19-
assert limit > 0 # in case of cycles or pathologically long synonym chains
19+
if not limit > 0: raise AssertionError(f"Could not find accepted taxon for synonymized taxon (id ='{taxon_id}')", {"taxonId" : taxon_id, "localizationKey" : "limitReachedDeterminingAccepted"})
2020
taxon_id = acceptedtaxon_id
2121
acceptedtaxon_id = Taxon.objects.select_for_update().values_list('acceptedtaxon_id', flat=True).get(id=taxon_id)
2222

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
from ..middleware.general import SpecifyExceptionWrapper
2+
from typing import Dict
3+
import traceback
4+
15
class BusinessRuleException(Exception):
26
pass
37

8+
class TreeBusinessRuleException(Exception):
9+
pass
10+
411
class AbortSave(Exception):
512
pass

specifyweb/businessrules/groupperson_rules.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
@orm_signal_handler('pre_save', 'Groupperson')
77
def agent_cannot_be_in_self(groupperson):
88
if groupperson.member_id == groupperson.group_id:
9-
raise BusinessRuleException('a group cannot be made a member of itself')
9+
raise BusinessRuleException(
10+
'a group cannot be made a member of itself',
11+
{"table" : "GroupPerson",
12+
"fieldName" : "member",
13+
"groupid" : groupperson.group_id})
1014

1115
@orm_signal_handler('pre_save', 'Groupperson')
1216
def grouppersion_pre_save(groupperson):

specifyweb/businessrules/interaction_rules.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,39 @@ def loanprep_quantity_must_be_lte_availability(ipreparation):
3434
quantity = ipreparation.quantity or 0
3535
quantityresolved = ipreparation.quantityresolved or 0
3636
if available < (quantity - quantityresolved):
37-
raise BusinessRuleException(f"loan preparation quantity exceeds availability ({ipreparation.id}: {quantity - quantityresolved} {available})")
37+
raise BusinessRuleException(
38+
f"loan preparation quantity exceeds availability ({ipreparation.id}: {quantity - quantityresolved} {available})",
39+
{"table" : "LoanPreparation",
40+
"fieldName" : "quantity",
41+
"preparationid" : ipreparation.id,
42+
"quantity" : quantity,
43+
"quantityresolved" : quantityresolved,
44+
"available" : available})
3845

3946
@orm_signal_handler('pre_save', 'Giftpreparation')
4047
def giftprep_quantity_must_be_lte_availability(ipreparation):
4148
if ipreparation.preparation is not None:
4249
available = get_availability(ipreparation.preparation, ipreparation.id, "giftpreparationid") or 0
4350
quantity = ipreparation.quantity or 0
4451
if available < quantity:
45-
raise BusinessRuleException(f"gift preparation quantity exceeds availability ({ipreparation.id}: {quantity} {available})")
52+
raise BusinessRuleException(
53+
f"gift preparation quantity exceeds availability ({ipreparation.id}: {quantity} {available})",
54+
{"table" : "GiftPreparation",
55+
"fieldName" : "quantity",
56+
"preparationid" : ipreparation.id,
57+
"quantity" : quantity,
58+
"available" : available})
4659

4760
@orm_signal_handler('pre_save', 'Exchangeoutprep')
4861
def exchangeoutprep_quantity_must_be_lte_availability(ipreparation):
4962
if ipreparation.preparation is not None:
5063
available = get_availability(ipreparation.preparation, ipreparation.id, "exchangeoutprepid") or 0
5164
quantity = ipreparation.quantity or 0
5265
if available < quantity:
53-
raise BusinessRuleException("exchangeout preparation quantity exceeds availability ({ipreparation.id}: {quantity} {available})")
66+
raise BusinessRuleException(
67+
"exchangeout preparation quantity exceeds availability ({ipreparation.id}: {quantity} {available})",
68+
{"table" : "ExchangeOutPrep",
69+
"fieldName" : "quantity",
70+
"preparationid" : ipreparation.id,
71+
"quantity" : quantity,
72+
"available" : available})

specifyweb/businessrules/tests/agent.py

-29
Original file line numberDiff line numberDiff line change
@@ -66,32 +66,3 @@ def test_agent_division_and_agenttype_cannot_be_null(self):
6666
firstname="Test",
6767
lastname="Agent",
6868
division=self.division)
69-
70-
71-
def test_other_and_group_do_not_have_addresses(self):
72-
from specifyweb.specify.agent_types import agent_types
73-
agent = models.Agent.objects.create(
74-
agenttype=agent_types.index('Person'),
75-
firstname="Test",
76-
lastname="Agent",
77-
division=self.division)
78-
79-
agent.addresses.create(address="somewhere")
80-
81-
models.Address.objects.get(agent=agent)
82-
83-
agent.agenttype = agent_types.index('Other')
84-
agent.save()
85-
86-
with self.assertRaises(models.Address.DoesNotExist):
87-
models.Address.objects.get(agent=agent)
88-
89-
agent.addresses.create(address="somewhere")
90-
91-
models.Address.objects.get(agent=agent)
92-
93-
agent.agenttype = agent_types.index('Group')
94-
agent.save()
95-
96-
with self.assertRaises(models.Address.DoesNotExist):
97-
models.Address.objects.get(agent=agent)
+8-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import logging
22

33
from .orm_signal_handler import orm_signal_handler
4-
from .exceptions import BusinessRuleException
4+
from .exceptions import TreeBusinessRuleException
55

66
logger = logging.getLogger(__name__)
77

88
@orm_signal_handler('pre_delete')
99
def cannot_delete_root_treedefitem(sender, obj):
1010
if hasattr(obj, 'treedef'): # is it a treedefitem?
1111
if sender.objects.get(id=obj.id).parent is None:
12-
raise BusinessRuleException("cannot delete root level tree definition item")
12+
raise TreeBusinessRuleException(
13+
"cannot delete root level tree definition item",
14+
{"tree" : obj.__class__.__name__,
15+
"localizationKey" : 'deletingTreeRoot',
16+
"node" : {
17+
"id" : obj.id
18+
}})
1319

specifyweb/businessrules/uniqueness_rules.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
def make_uniqueness_rule(model_name, parent_field, unique_field):
77
model = getattr(models, model_name)
8+
table_name = models.datamodel.get_table(model_name).name
89
if parent_field is None:
910
# uniqueness is global
1011
@orm_signal_handler('pre_save', model_name)
@@ -16,7 +17,13 @@ def check_unique(instance):
1617
if instance.id is not None:
1718
conflicts = conflicts.exclude(id=instance.id)
1819
if conflicts:
19-
raise BusinessRuleException("{} must have unique {}".format(model.__name__, unique_field))
20+
raise BusinessRuleException(
21+
"{} must have unique {}".format(table_name, unique_field),
22+
{"table" : table_name,
23+
"localizationKey" : "fieldNotUnique",
24+
"fieldName" : unique_field,
25+
"fieldData" : (unique_field, value),
26+
"conflicting" : list(conflicts.values_list('id', flat=True)[:100])})
2027
else:
2128
@orm_signal_handler('pre_save', model_name)
2229
def check_unique(instance):
@@ -34,7 +41,15 @@ def check_unique(instance):
3441
if instance.id is not None:
3542
conflicts = conflicts.exclude(id=instance.id)
3643
if conflicts:
37-
raise BusinessRuleException("{} must have unique {} in {}".format(model.__name__, unique_field, parent_field))
44+
raise BusinessRuleException(
45+
"{} must have unique {} in {}".format(table_name, unique_field, parent_field),
46+
{"table" : table_name,
47+
"localizationKey" : "childFieldNotUnique",
48+
"fieldName" : unique_field,
49+
"fieldData" : (unique_field, value),
50+
"parentField" : parent_field,
51+
"parentData" : f"{parent_field}: id={parent}",
52+
"conflicting" : list(conflicts.values_list('id', flat=True)[:100])})
3853
return check_unique
3954

4055
UNIQUENESS_RULES = {

specifyweb/businessrules/user_rules.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def deleting_user(sender, instance, **kwargs):
4343
nonpersonal_appresources = user.spappresources.filter(spappresourcedir__ispersonal=False)
4444
if nonpersonal_appresources.exists():
4545
raise BusinessRuleException(
46-
f"user {user.name} owns nonpersonal appresources {[r.name for r in nonpersonal_appresources]}"
46+
f"user {user.name} owns nonpersonal appresources {[r.name for r in nonpersonal_appresources]}",
47+
{"table" : user.__class__.__name__,
48+
"userid" : user.id}
4749
)
4850

4951
cursor = connection.cursor()

specifyweb/frontend/js_src/lib/components/Errors/FormatError.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { unsafeTriggerNotFound } from '../Router/Router';
1212
import { ErrorDialog } from './ErrorDialog';
1313
import { produceStackTrace } from './stackTrace';
1414
import { mainText } from '../../localization/main';
15+
import { formatJsonBackendResponse } from './JsonError';
1516

1617
export function formatError(
1718
error: unknown,
@@ -111,8 +112,7 @@ export function formatError(
111112
/** Format error message as JSON, HTML or plain text */
112113
function formatErrorResponse(error: string): JSX.Element {
113114
try {
114-
const json = JSON.parse(error);
115-
return <pre>{jsonStringify(json, 2)}</pre>;
115+
return formatJsonBackendResponse(error);
116116
} catch {
117117
// Failed parsing error message as JSON
118118
}

0 commit comments

Comments
 (0)