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
56 changes: 56 additions & 0 deletions specifyweb/backend/businessrules/rules/Loanprep_rules.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions specifyweb/backend/interactions/cog_preps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ExpressSearchConfigDialogProps = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly onSave?: () => void;
}
};

export function ExpressSearchConfigDialog({
isOpen,
Expand Down Expand Up @@ -76,7 +76,7 @@ export function ExpressSearchConfigDialog({
isOpen={isOpen}
onClose={onClose}
>
<ExpressSearchConfigEditor
<ExpressSearchConfigEditor
key={String(isOpen)}
onChangeJSON={handleChangeJSON}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -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;
Expand Down Expand Up @@ -86,7 +95,9 @@ export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onC

return (
<div className="flex flex-col gap-2 h-full min-h-[400px]">
<h3 className="font-bold mb-2">{expressSearchConfigText.configureResultsOrdering()}</h3>
<h3 className="font-bold mb-2">
{expressSearchConfigText.configureResultsOrdering()}
</h3>
<p className="text-sm text-gray-500 mb-4">
{expressSearchConfigText.reorderResultsOrderingDescription()}
</p>
Expand All @@ -99,7 +110,10 @@ export function ResultsOrderingTab({ config, relatedQueriesDefinitions = [], onC
>
<span className="font-medium">{item.label}</span>
<div className="flex gap-2">
<Button.BorderedGray disabled={index === 0} onClick={() => moveItem(index, 'up')}>
<Button.BorderedGray
disabled={index === 0}
onClick={() => moveItem(index, 'up')}
>
{icons.chevronUp}
</Button.BorderedGray>
<Button.BorderedGray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ const mockConfigResponse = {
tableName: 'CollectionObject',
displayOrder: 0,
searchFields: [],
displayFields: []
}
displayFields: [],
},
],
relatedQueries: []
relatedQueries: [],
},
related_queries_definitions: [],
schema_metadata: [
{
name: 'CollectionObject',
title: 'Collection Object',
fields: []
}
]
fields: [],
},
],
};

describe('ExpressSearchConfigEditor', () => {
Expand All @@ -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(
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
/>
);
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(
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
/>
);

expect(await findByRole('tablist')).toBeInTheDocument();
});

test('switches tabs correctly', async () => {
const { findByText, getByRole, user } = mount(
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
<ExpressSearchConfigEditor
onChange={jest.fn()}
onSetCleanup={jest.fn()}
/>
);

Expand All @@ -112,7 +113,7 @@ describe('ExpressSearchConfigEditor', () => {
await act(async () => {
await user.click(relatedTab);
});

expect(await findByText('Related Tables Tab')).toBeInTheDocument();

// Click Results Ordering
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ export function FormTable<SCHEMA extends AnySchema>({
resource.cid,
Boolean(
resource.specifyTable.name === 'Preparation' &&
collectionPreparationPref &&
resource.isNew()
collectionPreparationPref &&
resource.isNew()
),
])
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,13 @@ export function usePrimarySearch(
}

async function fetchRelatedSearches(): Promise<RA<string>> {
return contextUnlockedPromise.then(
async (entrypoint) =>
entrypoint === 'main'
? ajax<RA<string>>('/context/available_related_searches.json', {
headers: { Accept: 'application/json' },
cache: 'no-store',
}).then(({ data }) => data)
: foreverFetch<RA<string>>()
return contextUnlockedPromise.then(async (entrypoint) =>
entrypoint === 'main'
? ajax<RA<string>>('/context/available_related_searches.json', {
headers: { Accept: 'application/json' },
cache: 'no-store',
}).then(({ data }) => data)
: foreverFetch<RA<string>>()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,7 @@ function ExpressSearchInstructions({
{headerText.documentation()}
</Link.NewTab>
)}
<Button.Icon
icon="x"
title={commonText.close()}
onClick={onClose}
/>
<Button.Icon icon="x" title={commonText.close()} onClick={onClose} />
</div>
</div>
<ul className="mt-2 space-y-1 list-disc pl-5">
Expand All @@ -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') &&
Expand All @@ -176,11 +170,15 @@ export function ExpressSearchView(): JSX.Element {
<Button.Icon
icon="questionCircle"
title={commonText.expressSearchInstructionsTitle()}
onClick={(): void => setShowExpressSearchInstructions((value) => !value)}
onClick={(): void =>
setShowExpressSearchInstructions((value) => !value)
}
/>
</div>
{showInstructions && (
<ExpressSearchInstructions onClose={(): void => setShowExpressSearchInstructions(false)} />
<ExpressSearchInstructions
onClose={(): void => setShowExpressSearchInstructions(false)}
/>
)}
<Form onSubmit={(): void => setQuery(pendingQuery)}>
<div className="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,7 @@ export const notificationRenderers: IR<
);
},
'collection-creation-starting'() {
return (
<p>{setupToolText.collectionCreationStarted()}</p>
);
return <p>{setupToolText.collectionCreationStarted()}</p>;
},
default(notification) {
console.error('Unknown notification type', { notification });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,4 +713,4 @@ export function getMappingLineData({
: filtered.filter(
({ customSelectSubtype }) => customSelectSubtype !== 'tree'
);
}
}
9 changes: 6 additions & 3 deletions specifyweb/frontend/js_src/lib/localization/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,16 @@ export const commonText = createDictionary({
'hr-hr': 'Savjeti za pretraživanje',
},
expressSearchInstructions: {
'en-us': 'Separate multiple search terms with spaces, use % anywhere, * at the beginning or end, and wrap terms in quotes for exact multi-word matches.',
'en-us':
'Separate multiple search terms with spaces, use % anywhere, * at the beginning or end, and wrap terms in quotes for exact multi-word matches.',
},
expressSearchDateFormats: {
'en-us': 'Dates can be searched using either the YYYY-MM-DD or MM/DD/YYYY format.',
'en-us':
'Dates can be searched using either the YYYY-MM-DD or MM/DD/YYYY format.',
},
expressSearchPhraseExample: {
'en-us': 'To search a term with spaces, wrap the phrase in quotes, for example "Clinton Lake".',
'en-us':
'To search a term with spaces, wrap the phrase in quotes, for example "Clinton Lake".',
},
apply: {
'en-us': 'Apply',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const languageCodeMapper = {
'de-ch': 'de_CH',
'pt-br': 'pt_BR',
'hr-hr': 'hr',
'nb': 'nb_NO'
nb: 'nb_NO',
} as const;

export const languages = Object.keys(languageCodeMapper);
Expand Down
Loading
Loading