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
3 changes: 2 additions & 1 deletion specifyweb/backend/stored_queries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
path('make_recordset/', views.make_recordset),
path('merge_recordsets/', views.merge_recordsets),
path('return_loan_preps/', views.return_loan_preps),
path('batch_edit/', views.batch_edit)
path('batch_edit/', views.batch_edit),
path('query/<int:id>/ids/', views.query_ids),
]
35 changes: 35 additions & 0 deletions specifyweb/backend/stored_queries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,44 @@ def query(request, id):
limit=limit,
offset=offset
)


return HttpResponse(toJson(data), content_type='application/json')

@require_GET
@login_maybe_required
@never_cache
def query_ids(request, id):
"""Executes the query with id <id> and returns only the record IDs of the results as JSON."""
check_permission_targets(request.specify_collection.id, request.specify_user.id, [QueryBuilderPt.execute])
offset = int(request.GET.get('offset', 0))

with models.session_context() as session:
sp_query = session.query(models.SpQuery).get(int(id))
distinct = sp_query.selectDistinct
tableid = sp_query.contextTableId

if sp_query is None:
return HttpResponseBadRequest(f"SpQuery with id {id} does not exist.")

field_specs = [QueryField.from_spqueryfield(field, value_from_request(field, request.GET))
for field in sorted(sp_query.fields, key=lambda field: field.position)]

data = execute(
session=session,
collection=request.specify_collection,
user=request.specify_user,
tableid=tableid,
distinct=distinct,
series=False,
count_only=False,
field_specs=field_specs,
limit=None,
offset=offset
)

ids = [row[0] for row in data.get("results", [])]
return HttpResponse(toJson({ "ids": ids }), content_type='application/json')

@require_POST
@login_maybe_required
Expand Down
Binary file added specifyweb/frontend/.DS_Store
Binary file not shown.
79 changes: 79 additions & 0 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { useAsyncState } from '../../hooks/useAsyncState';
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
import { commonText } from '../../localization/common';
import { interactionsText } from '../../localization/interactions';
import {ajax} from "../../utils/ajax";
import { f } from '../../utils/functools';
import { type GetSet, type RA } from '../../utils/types';
import { Container, H3 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { LoadingContext } from '../Core/Contexts';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { schema } from '../DataModel/schema';
import type { SpecifyTable } from '../DataModel/specifyTable';
Expand Down Expand Up @@ -188,6 +190,8 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
typeof loadedResults?.[0]?.[0] === 'string' && loadedResults !== undefined;
const metaColumns = (showLineNumber ? 1 : 0) + 2;

const loading = React.useContext(LoadingContext);

return (
<Container.Base className="w-full !bg-[color:var(--form-background)]">
<div className="flex items-center items-stretch gap-2">
Expand All @@ -210,7 +214,45 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
>
{interactionsText.deselectAll()}
</Button.Small>


)}
{/* Buttons for select All and invert selection*/}
{(totalCount ?? 0) > 0 && (totalCount ?? 0) < 10_000_000 && queryResource?.get("id") && (
<Button.Small
onClick={(): void => {
loading(
fetchAllIDs(queryResource, loading)
.then((allIDs) => {
setSelectedRows(new Set(allIDs));
handleSelected?.(allIDs);
})
.catch((error) => {
console.error('Error fetching all IDs:', error);
})
);
}}
>
{interactionsText.selectAll()}
</Button.Small>
)}
{(totalCount ?? 0) > 0 && queryResource?.get("id") && (
<Button.Small
onClick={(): void => {
if (!loadedResults) return;
const allIDs = new Set(
loadedResults.map((result) => result[queryIdField] as number)
);
const invertedSelection = new Set( Array.from(allIDs).filter(id => !(selectedRows.has(id))));
setSelectedRows(invertedSelection);
handleSelected?.(Array.from(invertedSelection));

}}
>
{interactionsText.invertSelection()}
</Button.Small>
)}

<div className="-ml-2 flex-1" />
{displayedFields.length > 0 &&
visibleFieldSpecs.length > 0 &&
Expand Down Expand Up @@ -468,3 +510,40 @@ export function canMerge(table: SpecifyTable): boolean {
canMerge;
return canMergeOtherTables || canMergePaleoContext || canMergeCollectingEvent;
}

async function fetchAllIDs(
queryResource: SpecifyResource<SpQuery> | undefined,
loading: (promise: Promise<unknown>) => void
): Promise<RA<number>> {

const queryId = queryResource!.get('id');

if (!queryResource) {
throw new Error('Query resource is undefined');
}

return new Promise<RA<number>>((resolve, reject) => {
loading(
(async () => {
try {
console.log('Fetching all IDs for query');
const startTime = performance.now();
const {data} = await ajax<{readonly ids: RA<number>}>(
`/stored_query/query/${queryId}/ids/`,
{
headers: { Accept: 'application/json' },
errorMode: "visible",
}
);

const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
console.log(`Fetched ${data.ids.length} IDs in ${elapsed} seconds`);

resolve(data.ids);
} catch (error) {
reject(error);
}
})()
);
});
}
3 changes: 3 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,7 @@ export const interactionsText = createDictionary({
'ru-ru': 'Нет в наличии',
'uk-ua': 'Не доступно',
},
invertSelection: {
"en-us": "Invert Selection",
}
} as const);