Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow COG selection for interactions #5445

Draft
wants to merge 13 commits into
base: production
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { H3 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { Link } from '../Atoms/Link';
import { LoadingContext, ReadOnlyContext } from '../Core/Contexts';
import { fetchCollection } from '../DataModel/collection';
import type {
AnyInteractionPreparation,
AnySchema,
Expand All @@ -33,14 +34,15 @@ import type {
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { getResourceViewUrl } from '../DataModel/resource';
import type { LiteralField } from '../DataModel/specifyField';
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
import { tables } from '../DataModel/tables';
import type { Collection } from '../DataModel/specifyTable';
import { getTableById, tables } from '../DataModel/tables';
import type {
DisposalPreparation,
GiftPreparation,
LoanPreparation,
RecordSet,
} from '../DataModel/types';
import { softError } from '../Errors/assert';
import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea';
import { Dialog } from '../Molecules/Dialog';
import { userPreferences } from '../Preferences/userPreferences';
Expand Down Expand Up @@ -112,8 +114,43 @@ export function InteractionDialog({
recordSet: SerializedResource<RecordSet> | undefined
): void {
const catalogNumbers = handleParse();
if (catalogNumbers === undefined) return undefined;
if (isLoanReturn)
const recordSetTable =
typeof recordSet?.dbTableId === 'number'
? getTableById(recordSet.dbTableId)
: null;

const rsId = recordSet?.id;

if (recordSetTable?.name === 'CollectionObjectGroup') {
fetchCollection('RecordSetItem', {
recordSet: rsId,
domainFilter: false,
limit: 2000,
})
.then(({ records }) => {
const catIdsFromRS = records.map((record) =>
record.recordId.toString()
);

handleProceedWithCatalogNumbers(catIdsFromRS, recordSet);
})
.catch((error) => {
softError(
'Error fetching catalog numbers from RecordSetItem:',
error
);
});
} else {
handleProceedWithCatalogNumbers(catalogNumbers, recordSet);
}
}

function handleProceedWithCatalogNumbers(
catalogNumbers: RA<string> | undefined,
recordSet: SerializedResource<RecordSet> | undefined
): void {
if (catalogNumbers === undefined) return;
if (isLoanReturn) {
loading(
ajax<readonly [preprsReturned: number, loansClosed: number]>(
'/interactions/loan_return_all/',
Expand All @@ -133,13 +170,13 @@ export function InteractionDialog({
})
)
);
else if (typeof recordSet === 'object')
} else if (typeof recordSet === 'object') {
loading(
getPrepsAvailableForLoanRs(recordSet.id, isLoan).then((data) =>
availablePrepsReady(undefined, data)
)
);
else
} else {
loading(
(catalogNumbers.length === 0
? Promise.resolve([])
Expand All @@ -150,6 +187,7 @@ export function InteractionDialog({
)
).then((data) => availablePrepsReady(catalogNumbers, data))
);
}
}

const [prepsData, setPrepsData] = React.useState<RA<PreparationRow>>();
Expand Down Expand Up @@ -293,6 +331,9 @@ export function InteractionDialog({
) : (
<ReadOnlyContext.Provider value>
<RecordSetsDialog
collectionObjectGroupResourceTableId={
new tables.CollectionObjectGroup.Resource().specifyTable.tableId
}
table={itemTable}
onClose={handleClose}
onSelect={handleProceed}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export function PrepDialog({
})
);

// call api create_sibling_loan_preps

if (typeof itemCollection === 'object') {
itemCollection.add(items);
handleClose();
Expand Down
38 changes: 36 additions & 2 deletions specifyweb/frontend/js_src/lib/components/Toolbar/RecordSets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { schema } from '../DataModel/schema';
import { deserializeResource } from '../DataModel/serializers';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { getTableById, tables } from '../DataModel/tables';
import type { RecordSet } from '../DataModel/types';
import type { CollectionObjectGroup, RecordSet } from '../DataModel/types';
import { userInformation } from '../InitialContext/userInformation';
import { loadingGif } from '../Molecules';
import { DateElement } from '../Molecules/DateElement';
Expand Down Expand Up @@ -58,12 +58,14 @@ export function RecordSetsDialog({
onConfigure: handleConfigure,
onSelect: handleSelect,
children = defaultRenderer,
collectionObjectGroupResourceTableId,
}: {
readonly onClose: () => void;
readonly table?: SpecifyTable;
readonly onConfigure?: (recordSet: SerializedResource<RecordSet>) => void;
readonly onSelect?: (recordSet: SerializedResource<RecordSet>) => void;
readonly children?: Renderer;
readonly collectionObjectGroupResourceTableId?: number;
}): JSX.Element | null {
const [state, setState] = React.useState<
| State<'CreateState'>
Expand Down Expand Up @@ -101,6 +103,38 @@ export function RecordSetsDialog({
false
);

const [collectionObjectGroupData] = useAsyncState(
React.useCallback(
/**
* DomainFilter does filter for tables that are
* scoped using the collectionMemberId field
*/
async () =>
fetchCollection('RecordSet', {
specifyUser: userInformation.id,
type: 0,
limit,
domainFilter: true,
orderBy,
offset,
dbTableId: collectionObjectGroupResourceTableId,
collectionMemberId: schema.domainLevelIds.collection,
}),
[collectionObjectGroupResourceTableId, limit, offset, orderBy]
),
false
);

const concatenatedRecordSets = [
...(data?.records ?? []),
...(collectionObjectGroupData?.records ?? []),
];

const RSToUse =
typeof collectionObjectGroupResourceTableId === 'number'
? concatenatedRecordSets
: data?.records;

const totalCountRef = React.useRef<number | undefined>(undefined);
totalCountRef.current = data?.totalCount ?? totalCountRef.current;
const totalCount = totalCountRef.current;
Expand Down Expand Up @@ -140,7 +174,7 @@ export function RecordSetsDialog({
</tr>
</thead>
<tbody>
{data?.records.map((recordSet) => (
{RSToUse?.map((recordSet) => (
<Row
key={recordSet.id}
recordSet={recordSet}
Expand Down
176 changes: 176 additions & 0 deletions specifyweb/interactions/cog_preps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from typing import Any, List, Optional, Set
from django.db.models import Subquery
from django.db.models.query import QuerySet
from specifyweb.specify.models import Collectionobject, Collectionobjectgroup, Collectionobjectgroupjoin, Loan, Loanpreparation, Preparation, Recordset, Recordsetitem
from specifyweb.specify.models_by_table_id import get_table_id_by_model_name

def get_cog_consolidated_preps(cog: Collectionobjectgroup) -> List[Preparation]:
"""
Recursively get all the child CollectionObjectGroups, then get the leaf CollectionObjects,
and then reuturn all the preparation ids if the CollectionObjectGroup to CollectionObject is consolidated
"""
# Don't continue if the cog is not consolidated
if cog.cogtype is None or cog.cogtype.type is None or cog.cogtype.type.lower().title() != 'Consolidated':
return []

# For each child cog, recursively get the consolidated preparations
child_cogs = Collectionobjectgroupjoin.objects.filter(
parentcog=cog, childcog__isnull=False
).values_list("childcog", flat=True)
consolidated_preps = []
for child_cog in child_cogs:
child_preps = get_cog_consolidated_preps(child_cog)
consolidated_preps.extend(child_preps)

# Get the child CollectionObjects
collection_objects = Collectionobjectgroupjoin.objects.filter(
parentcog=cog, childco__isnull=False
).values_list("childco", flat=True)

# For each CollectionObject, get the preparations
for co in collection_objects:
consolidated_preps.extend(Preparation.objects.filter(collectionobject=co))

return consolidated_preps

def get_the_top_consolidated_parent_cog_of_prep(prep: Preparation) -> Optional[Collectionobjectgroup]:
"""
Get the topmost consolidated parent CollectionObjectGroup of the preparation
"""
# Get the CollectionObject of the preparation
co = prep.collectionobject
if co is None:
return None

# Get the parent cog of the CollectionObject
cog = Collectionobjectgroupjoin.objects.filter(childco=co).values_list("parentcog", flat=True).first()
if cog is None:
return None

cojo = Collectionobjectgroupjoin.objects.filter(childcog=cog).first()
consolidated_parent_cog = cojo.parentcog if cojo is not None else None
top_cog = consolidated_parent_cog

# Move up consolidated parent CollectionObjectGroups until the top consolidated CollectionObjectGroup is found
while consolidated_parent_cog is not None:
if consolidated_parent_cog.cogtype is None or consolidated_parent_cog.cogtype.type is None or consolidated_parent_cog.cogtype.type.lower().title() != 'Consolidated':
break
top_cog = consolidated_parent_cog
cojo = Collectionobjectgroupjoin.objects.filter(childcog=consolidated_parent_cog).first()
consolidated_parent_cog = cojo.parentcog if cojo is not None else None

return top_cog

def get_all_sibling_preps_within_consolidated_cog(prep: Preparation) -> List[Preparation]:
"""
Get all the sibling preparations within the consolidated cog
"""
# Get the topmost consolidated parent cog of the preparation
top_consolidated_cog = get_the_top_consolidated_parent_cog_of_prep(prep)
if top_consolidated_cog is None:
return [prep]

# Get all the sibling preparations
sibling_preps = get_cog_consolidated_preps(top_consolidated_cog)
# preps.extend(sibling_preps)

# Dedup the list
preps = list(set(preps))

# return preps
return sibling_preps

def remove_all_cog_sibling_preps_from_loan(prep: Preparation, loan: Loan) -> None:
"""
Remove all the sibling preparations within the consolidated cog
"""
# Get all the sibling preparations
preps = get_all_sibling_preps_within_consolidated_cog(prep)

# Get the loan preparations
loan_preps = Loanpreparation.objects.filter(loan=loan, preparation__in=preps)

# Delete the loan preparations
loan_preps.delete()

def is_cog_recordset(rs: Recordset) -> bool:
"""
Check if the recordset is a CollectionObjectGroup recordset
"""
return rs.dbtableid == get_table_id_by_model_name('Collectionobjectgroup')

def is_co_recordset(rs: Recordset) -> bool:
"""
Check if the recordset is a CollectionObjectGroup recordset
"""
return rs.dbtableid == get_table_id_by_model_name('Collectionobject')

def get_cogs_from_co_recordset(rs: Recordset) -> Optional[QuerySet[Collectionobjectgroup]]:
"""
Get the CollectionObjectGroups from the CollectionObject recordset
"""

if not is_co_recordset(rs):
return None

# Subquery to get CollectionObjectIDs from the recordset
co_subquery = Recordsetitem.objects.filter(recordset=rs).values('recordid')

# Subquery to get parentcog IDs from Collectionobjectgroupjoin
parent_cog_subquery = Collectionobjectgroupjoin.objects.filter(
childco__in=Subquery(co_subquery)
).values('parentcog')

# Main query to get Collectionobjectgroup objects
cogs = Collectionobjectgroup.objects.filter(id__in=Subquery(parent_cog_subquery))
return cogs

def get_cogs_from_co_ids(co_ids: List[int]) -> Optional[QuerySet[Collectionobjectgroup]]:
"""
Get the CollectionObjectGroups from the CollectionObject IDs
"""
# Subquery to get parentcog IDs from Collectionobjectgroupjoin
parent_cog_subquery = Collectionobjectgroupjoin.objects.filter(
childco__in=co_ids
).values('parentcog')

# Main query to get Collectionobjectgroup objects
cogs = Collectionobjectgroup.objects.filter(id__in=Subquery(parent_cog_subquery))
return cogs

def get_cog_consolidated_preps_co_ids(cog: Collectionobjectgroup) -> Set[Collectionobject]:
preps = get_cog_consolidated_preps(cog)

# Return set of distinct CollectionObjectIDs associated with the preparations
return set(prep.collectionobject.id for prep in preps)

def add_consolidated_sibling_co_ids(request_co_ids: List[Any], id_fld: Optional[str]=None) -> List[Any]:
"""
Get the consolidated sibling CO IDs of the COs in the list
"""
cog_sibling_co_ids = set()
if id_fld is None:
# id_fld = 'id'
id_fld = 'catalognumber'
id_fld = id_fld.lower()
co_ids = Collectionobject.objects.filter(**{f"{id_fld}__in": request_co_ids}).values_list('id', flat=True)
cogs = get_cogs_from_co_ids(co_ids)
for cog in cogs:
cog_sibling_co_ids.update(get_cog_consolidated_preps_co_ids(cog))
# cog_sibling_co_ids -= set(co_ids)

cog_sibling_co_idfld_ids = Collectionobject.objects.filter(id__in=cog_sibling_co_ids).values_list(id_fld, flat=True)
return list(set(request_co_ids).union(set(cog_sibling_co_idfld_ids)))

def get_consolidated_co_siblings_from_rs(rs: Recordset) -> Set[Collectionobject]:
"""
Get the consolidated sibling CO IDs of the COs in the recordset
"""
cog_sibling_co_ids = set()
if is_co_recordset(rs):
cogs = get_cogs_from_co_recordset(rs)
for cog in cogs:
cog_sibling_co_ids.union(get_cog_consolidated_preps_co_ids(cog))
# cog_sibling_co_ids -= set(rs.recordsetitems.values_list('recordid', flat=True))

return cog_sibling_co_ids
5 changes: 5 additions & 0 deletions specifyweb/interactions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@
url(r'^prep_interactions/', prep_interactions),
url(r'^prep_availability/(?P<prep_id>\d+)/(?P<iprep_id>\d+)/(?P<iprep_name>\w+)/', prep_availability),
url(r'^prep_availability/(?P<prep_id>\d+)/', prep_availability),

# special COG APIs
url(r'^cog_consolidated_preps/(?P<model>\w+)/$', cog_consolidated_preps),
url(r'^remove_cog_consolidated_preps/(?P<model>\w+)/$', remove_cog_consolidated_preps),
url(r'^create_sibling_loan_preps/(?P<model>\w+)/$', create_sibling_loan_preps),
]
Loading
Loading