diff --git a/specifyweb/backend/stored_queries/execution.py b/specifyweb/backend/stored_queries/execution.py index fcb67ebd9b5..83eb2058501 100644 --- a/specifyweb/backend/stored_queries/execution.py +++ b/specifyweb/backend/stored_queries/execution.py @@ -684,7 +684,14 @@ def recordset(collection, user, user_agent, recordset_info): # pragma: no cover field_specs = fields_from_json(spquery["fields"]) - query, __ = build_query(session, collection, user, tableid, field_specs) + query, __ = build_query( + session, + collection, + user, + tableid, + field_specs, + BuildQueryProps(recordsetid=spquery.get("recordsetid", None)), + ) query = query.with_entities(model._id, literal(new_rs_id)).distinct() RSI = models.RecordSetItem ins = insert(RSI).from_select((RSI.recordId, RSI.RecordSetID), query) @@ -973,7 +980,25 @@ def build_query( if props.recordsetid is not None: logger.debug("joining query to recordset: %s", props.recordsetid) recordset = session.query(models.RecordSet).get(props.recordsetid) - if not (recordset.dbTableId == tableid): + if recordset is None: + raise AssertionError( + f"Unexpected recordset id '{props.recordsetid}' in request. Recordset not found.", + { + "recordsetId": props.recordsetid, + "localizationKey": "unexpectedRecordsetId", + }, + ) + if recordset.collectionMemberId != collection.id: + raise AssertionError( + f"Unexpected recordset id '{props.recordsetid}' in request. Recordset is not in collection '{collection.id}'.", + { + "recordsetId": props.recordsetid, + "collectionId": collection.id, + "expectedCollectionId": recordset.collectionMemberId, + "localizationKey": "unexpectedRecordsetCollection", + }, + ) + if recordset.dbTableId != tableid: raise AssertionError( f"Unexpected tableId '{tableid}' in request. Expected '{recordset.dbTableId}'", { diff --git a/specifyweb/backend/stored_queries/tests/test_recordset.py b/specifyweb/backend/stored_queries/tests/test_recordset.py new file mode 100644 index 00000000000..b1c49290ce0 --- /dev/null +++ b/specifyweb/backend/stored_queries/tests/test_recordset.py @@ -0,0 +1,53 @@ +from unittest import TestCase +from unittest.mock import Mock, patch + +from specifyweb.backend.stored_queries.execution import BuildQueryProps, recordset + +class TestRecordSet(TestCase): + @patch("specifyweb.backend.stored_queries.execution.insert") + @patch("specifyweb.backend.stored_queries.execution.build_query") + @patch("specifyweb.backend.stored_queries.execution.fields_from_json") + @patch("specifyweb.backend.stored_queries.execution.models.session_context") + @patch("specifyweb.backend.stored_queries.execution.models.RecordSet") + def test_query_is_scoped_to_source_recordset( + self, + recordset_model, + session_context, + fields_from_json, + build_query, + _insert, + ): + collection = Mock(id=1) + user = Mock(id=2) + user_agent = Mock(id=3) + source_recordset_id = 4 + field_specs = [Mock()] + query = Mock() + + session = session_context.return_value.__enter__.return_value + recordset_model.return_value.recordSetId = 5 + fields_from_json.return_value = field_specs + build_query.return_value = (query, Mock()) + + recordset( + collection, + user, + user_agent, + { + "name": "Filtered Record Set", + "fromquery": { + "contexttableid": 1, + "fields": [], + "recordsetid": source_recordset_id, + }, + }, + ) + + build_query.assert_called_once_with( + session, + collection, + user, + 1, + field_specs, + BuildQueryProps(recordsetid=source_recordset_id), + ) \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Components.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Components.tsx index 293c84b9ff8..7d0e5331aa7 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Components.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Components.tsx @@ -188,6 +188,7 @@ export function MakeRecordSetButton({ queryResource, fields, getQueryFieldRecords, + sourceRecordSetId, }: { readonly baseTableName: keyof Tables; readonly queryResource: SpecifyResource; @@ -195,6 +196,7 @@ export function MakeRecordSetButton({ readonly getQueryFieldRecords: | (() => RA>) | undefined; + readonly sourceRecordSetId: number | undefined; }): JSX.Element { const [state, setState] = React.useState< 'editing' | 'saved' | 'saving' | undefined @@ -222,7 +224,10 @@ export function MakeRecordSetButton({ recordSet.set('dbTableId', strictGetTable(baseTableName).tableId); // @ts-expect-error Adding a non-datamodel field - recordSet.set('fromQuery', queryResource.toJSON()); + recordSet.set('fromQuery', { + ...queryResource.toJSON(), + recordsetid: sourceRecordSetId, + }); // @ts-expect-error Overwriting the resource back-end URL recordSet.url = '/stored_query/make_recordset/'; setRecordSet(recordSet); diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx index 446a7ce0bcb..80c360c86de 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx @@ -625,6 +625,7 @@ function Wrapped({ fields={state.fields} getQueryFieldRecords={getQueryFieldRecords} queryResource={queryResource} + sourceRecordSetId={recordSet?.id} /> ) : undefined }