Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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.
85 changes: 85 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,51 @@ 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 && (totalCount ?? 0) < 3_000_000 && queryResource?.get("id") && (
<Button.Small
onClick={(): void => {
loading(
fetchAllIDs(queryResource,loading)
.then((allIDs) => {
if (!loadedResults) return;
const invertedSelection = new Set( Array.from(allIDs).filter(id => !(selectedRows.has(id))));
setSelectedRows(invertedSelection);
handleSelected?.(Array.from(invertedSelection));

})
.catch((error) => {
console.error("Error fetchign all IDs", error);
})
);

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

<div className="-ml-2 flex-1" />
{displayedFields.length > 0 &&
visibleFieldSpecs.length > 0 &&
Expand Down Expand Up @@ -468,3 +516,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);