diff --git a/specifyweb/backend/businessrules/rules/Loanprep_rules.py b/specifyweb/backend/businessrules/rules/Loanprep_rules.py
new file mode 100644
index 00000000000..62fbcdfd8a0
--- /dev/null
+++ b/specifyweb/backend/businessrules/rules/Loanprep_rules.py
@@ -0,0 +1,56 @@
+import logging
+from functools import wraps
+from django.apps import apps
+
+logger = logging.getLogger(__name__)
+
+def orm_signal_handler(signal_name, model_name=None, dispatch_uid=None):
+ """Decorator for Django ORM signal handlers."""
+ def decorator(rule):
+ @wraps(rule)
+ def handler(sender=None, instance=None, **kwargs):
+ # Handle both standard signal dispatch and edge cases
+ obj = instance or kwargs.get('instance')
+
+ try:
+ rule(obj)
+ except Exception as e:
+ logger.exception(f"Error in {rule.__name__} for {obj.__class__.__name__}")
+ raise
+
+ # connect the signal handler
+ try:
+ from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
+
+ signal_map = {
+ 'pre_save': pre_save,
+ 'post_save': post_save,
+ 'pre_delete': pre_delete,
+ 'post_delete': post_delete,
+ }
+ signal = signal_map.get(signal_name)
+
+ if signal is None:
+ raise ValueError(f"Unknown signal: {signal_name}")
+
+ if model_name is not None:
+ try:
+ model = apps.get_model('specify', model_name)
+ signal.connect(
+ handler,
+ sender=model,
+ dispatch_uid=dispatch_uid or f"{rule.__module__}.{rule.__name__}"
+ )
+ except LookupError:
+ logger.debug(f"Model {model_name} not found, skipping signal handler registration")
+ else:
+ signal.connect(
+ handler,
+ dispatch_uid=dispatch_uid or f"{rule.__module__}.{rule.__name__}"
+ )
+ except Exception as e:
+ logger.exception(f"Failed to register signal handler {rule.__name__}")
+
+ return handler
+
+ return decorator
\ No newline at end of file
diff --git a/specifyweb/backend/interactions/cog_preps.py b/specifyweb/backend/interactions/cog_preps.py
index 63089be38da..115cafed5cf 100644
--- a/specifyweb/backend/interactions/cog_preps.py
+++ b/specifyweb/backend/interactions/cog_preps.py
@@ -546,10 +546,11 @@ def modify_update_of_loan_return_sibling_preps(original_interaction_obj, updated
in updated_interaction_data["loanpreparations"][loan_prep_idx].keys()
else []
)
+ #fixed error firefox specific error caused by quantity returned being posted as a string instead of an int in the API request, need to convert to int before summing
total_quantity_returned = sum(
- [loan_return["quantityreturned"] for loan_return in loan_return_data])
+ [int(loan_return["quantityreturned"]) for loan_return in loan_return_data])
total_quantity_resolved = sum(
- [loan_return["quantityresolved"] for loan_return in loan_return_data])
+ [int(loan_return["quantityresolved"]) for loan_return in loan_return_data])
updated_interaction_data["loanpreparations"][loan_prep_idx]["quantityresolved"] = total_quantity_resolved
updated_interaction_data["loanpreparations"][loan_prep_idx]["quantityreturned"] = total_quantity_returned
diff --git a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ExpressSearchConfigDialog.tsx b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ExpressSearchConfigDialog.tsx
index 6c5b8a531b5..81cc07e0bd3 100644
--- a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ExpressSearchConfigDialog.tsx
+++ b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ExpressSearchConfigDialog.tsx
@@ -14,7 +14,7 @@ type ExpressSearchConfigDialogProps = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly onSave?: () => void;
-}
+};
export function ExpressSearchConfigDialog({
isOpen,
@@ -76,7 +76,7 @@ export function ExpressSearchConfigDialog({
isOpen={isOpen}
onClose={onClose}
>
-
diff --git a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ResultsOrderingTab.tsx b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ResultsOrderingTab.tsx
index 4478fd66f5a..437e55abc1b 100644
--- a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ResultsOrderingTab.tsx
+++ b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/ResultsOrderingTab.tsx
@@ -11,12 +11,17 @@ import { genericTables } from '../DataModel/tables';
function tableLabel(tableName: string): string {
return (
- (genericTables[tableName as keyof typeof genericTables]?.label as string | undefined) ??
- camelToHuman(tableName)
+ (genericTables[tableName as keyof typeof genericTables]?.label as
+ | string
+ | undefined) ?? camelToHuman(tableName)
);
}
-export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onChangeConfig }: any) {
+export function ResultsOrderingTab({
+ config,
+ relatedQueriesDefinitions = [],
+ onChangeConfig,
+}: any) {
const baseTables = config.tables
.filter((t: any) => t.searchFields.some((sf: any) => sf.inUse !== false))
.map((t: any) => ({
@@ -29,8 +34,12 @@ export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onC
const activeQueries = config.relatedQueries
.filter((rq: any) => rq.isActive)
.map((rq: any) => {
- const def = relatedQueriesDefinitions.find((def: any) => def.id === rq.id);
- const title = def?.name ? getExpressSearchQueryTitle(def.name) : undefined;
+ const def = relatedQueriesDefinitions.find(
+ (def: any) => def.id === rq.id
+ );
+ const title = def?.name
+ ? getExpressSearchQueryTitle(def.name)
+ : undefined;
if (!def || !title || title === String(def.name)) {
return undefined;
@@ -86,7 +95,9 @@ export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onC
return (
-
{expressSearchConfigText.configureResultsOrdering()}
+
+ {expressSearchConfigText.configureResultsOrdering()}
+
{expressSearchConfigText.reorderResultsOrderingDescription()}
@@ -99,7 +110,10 @@ export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onC
>
{item.label}
- moveItem(index, 'up')}>
+ moveItem(index, 'up')}
+ >
{icons.chevronUp}
{
@@ -65,42 +65,43 @@ describe('ExpressSearchConfigEditor', () => {
});
expect(onChangeJSON).toHaveBeenCalled();
- const latestConfig = onChangeJSON.mock.calls[onChangeJSON.mock.calls.length - 1][0];
+ const latestConfig =
+ onChangeJSON.mock.calls[onChangeJSON.mock.calls.length - 1][0];
expect(latestConfig.tables[0].tableName).toBe('Agent');
expect(latestConfig.tables[0].searchFields[0].fieldName).toBe('firstName');
});
test('renders loading state initially', async () => {
const { getByText } = mount(
-
);
expect(getByText('Loading...')).toBeInTheDocument();
-
+
// Wait for it to finish loading to avoid act warnings
await act(async () => {
- await new Promise(resolve => setTimeout(resolve, 0));
+ await new Promise((resolve) => setTimeout(resolve, 0));
});
});
test('renders tabs after data load', async () => {
const { findByRole } = mount(
-
);
-
+
expect(await findByRole('tablist')).toBeInTheDocument();
});
test('switches tabs correctly', async () => {
const { findByText, getByRole, user } = mount(
-
);
@@ -112,7 +113,7 @@ describe('ExpressSearchConfigEditor', () => {
await act(async () => {
await user.click(relatedTab);
});
-
+
expect(await findByText('Related Tables Tab')).toBeInTheDocument();
// Click Results Ordering
diff --git a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/RelatedTablesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/RelatedTablesTab.test.tsx
index bacba609689..e096a045a54 100644
--- a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/RelatedTablesTab.test.tsx
+++ b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/RelatedTablesTab.test.tsx
@@ -43,7 +43,9 @@ describe('RelatedTablesTab', () => {
expect(onChangeConfig).toHaveBeenCalledTimes(1);
const newConfig = onChangeConfig.mock.calls[0][0];
- expect(newConfig.relatedQueries.find((rq: any) => rq.id === '2').isActive).toBe(true);
+ expect(
+ newConfig.relatedQueries.find((rq: any) => rq.id === '2').isActive
+ ).toBe(true);
const activeRow = rows[0];
const activeCheckbox = activeRow.querySelector('input[type="checkbox"]');
@@ -54,6 +56,8 @@ describe('RelatedTablesTab', () => {
expect(onChangeConfig).toHaveBeenCalledTimes(2);
const secondConfig = onChangeConfig.mock.calls[1][0];
- expect(secondConfig.relatedQueries.find((rq: any) => rq.id === '1').isActive).toBe(false);
+ expect(
+ secondConfig.relatedQueries.find((rq: any) => rq.id === '1').isActive
+ ).toBe(false);
});
});
diff --git a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/ResultsOrderingTab.test.tsx b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/ResultsOrderingTab.test.tsx
index 5997fc65f7c..4b15f595931 100644
--- a/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/ResultsOrderingTab.test.tsx
+++ b/specifyweb/frontend/js_src/lib/components/ExpressSearchConfig/__tests__/ResultsOrderingTab.test.tsx
@@ -39,9 +39,7 @@ describe('ResultsOrderingTab', () => {
displayFields: [],
},
],
- relatedQueries: [
- { id: '8', isActive: true, displayOrder: 1 },
- ],
+ relatedQueries: [{ id: '8', isActive: true, displayOrder: 1 }],
};
const onChangeConfig = jest.fn();
diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx
index 0eaae790141..baf13a48768 100644
--- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx
+++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx
@@ -214,8 +214,8 @@ export function FormTable({
resource.cid,
Boolean(
resource.specifyTable.name === 'Preparation' &&
- collectionPreparationPref &&
- resource.isNew()
+ collectionPreparationPref &&
+ resource.isNew()
),
])
)
diff --git a/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchHooks.tsx b/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchHooks.tsx
index a59d68078c7..62177d62253 100644
--- a/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchHooks.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchHooks.tsx
@@ -52,14 +52,13 @@ export function usePrimarySearch(
}
async function fetchRelatedSearches(): Promise> {
- return contextUnlockedPromise.then(
- async (entrypoint) =>
- entrypoint === 'main'
- ? ajax>('/context/available_related_searches.json', {
- headers: { Accept: 'application/json' },
- cache: 'no-store',
- }).then(({ data }) => data)
- : foreverFetch>()
+ return contextUnlockedPromise.then(async (entrypoint) =>
+ entrypoint === 'main'
+ ? ajax>('/context/available_related_searches.json', {
+ headers: { Accept: 'application/json' },
+ cache: 'no-store',
+ }).then(({ data }) => data)
+ : foreverFetch>()
);
}
diff --git a/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchTask.tsx b/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchTask.tsx
index 60686c9bccf..b392a409b6d 100644
--- a/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchTask.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Header/ExpressSearchTask.tsx
@@ -125,11 +125,7 @@ function ExpressSearchInstructions({
{headerText.documentation()}
)}
-
+
@@ -147,10 +143,8 @@ export function ExpressSearchView(): JSX.Element {
const [pendingQuery] = value;
const [isConfigOpen, setIsConfigOpen] = React.useState(false);
const [configRefreshTrigger, setConfigRefreshTrigger] = React.useState(0);
- const [showInstructions = true, setShowExpressSearchInstructions] = useCachedState(
- 'expressSearch',
- 'showSearchTips'
- );
+ const [showInstructions = true, setShowExpressSearchInstructions] =
+ useCachedState('expressSearch', 'showSearchTips');
const canEditExpressSearchConfig =
hasToolPermission('resources', 'read') &&
hasToolPermission('resources', 'create') &&
@@ -176,11 +170,15 @@ export function ExpressSearchView(): JSX.Element {
setShowExpressSearchInstructions((value) => !value)}
+ onClick={(): void =>
+ setShowExpressSearchInstructions((value) => !value)
+ }
/>
{showInstructions && (
- setShowExpressSearchInstructions(false)} />
+ setShowExpressSearchInstructions(false)}
+ />
)}