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()} )} - +