Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from specifyweb.backend.businessrules.exceptions import BusinessRuleException
from specifyweb.backend.businessrules.utils import get_unique_catnum_across_comp_co_coll_pref
from specifyweb.specify.api.utils import ensure_collection_object_type
from specifyweb.specify.models import Component


Expand All @@ -10,8 +11,7 @@ def collectionobject_pre_save(co):
if co.collectionmemberid is None:
co.collectionmemberid = co.collection_id

if co.collectionobjecttype is None:
co.collectionobjecttype = co.collection.collectionobjecttype
ensure_collection_object_type(co, using=co._state.db or 'default')

agent = co.createdbyagent
if agent is not None and agent.specifyuser is not None:
Expand All @@ -25,4 +25,4 @@ def collectionobject_pre_save(co):

if contains_component_duplicates:
raise BusinessRuleException(
'Catalog Number is already in use for another Component in this collection.')
'Catalog Number is already in use for another Component in this collection.')
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from specifyweb.backend.businessrules.orm_signal_handler import orm_signal_handler

from specifyweb.specify.api.utils import ensure_collection_object_type
from specifyweb.specify.models import Determination, Taxon


Expand All @@ -8,6 +9,12 @@ def determination_pre_save(det):
if det.collectionmemberid is None:
det.collectionmemberid = det.collectionobject.collectionmemberid

ensure_collection_object_type(
det.collectionobject,
using=det._state.db or 'default',
persist=True,
)

taxon_id = det.taxon_id
if taxon_id is None:
det.preferredtaxon = None
Expand All @@ -33,4 +40,4 @@ def determination_pre_save(det):
def only_one_determination_iscurrent(determination):
if determination.iscurrent:
Determination.objects.filter(
collectionobject=determination.collectionobject_id).update(iscurrent=False)
collectionobject=determination.collectionobject_id).update(iscurrent=False)
117 changes: 117 additions & 0 deletions specifyweb/backend/businessrules/tests/test_collectionobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,120 @@ def test_default_collectionobjecttype(self):

self.assertIsNotNone(test_co.collectionobjecttype)
self.assertEqual(test_co.collectionobjecttype, default_type)

def test_new_collectionobject_without_collection_default_keeps_null_type(self):
test_co = Collectionobject.objects.create(
collection=self.collection
)

self.assertIsNone(test_co.collectionobjecttype)

def test_existing_collectionobject_without_type_uses_collection_default_on_save(self):
default_type = Collectionobjecttype.objects.create(
name="default type",
collection=self.collection,
taxontreedef=self.discipline.taxontreedef
)
self.collection.collectionobjecttype = default_type
self.collection.save()

test_co = self.collectionobjects[0]
Collectionobject.objects.filter(pk=test_co.pk).update(
collectionobjecttype=None
)

test_co.refresh_from_db()
self.assertIsNone(test_co.collectionobjecttype)

test_co.save()
test_co.refresh_from_db()

self.assertEqual(test_co.collectionobjecttype, default_type)

def test_existing_collectionobject_without_type_creates_collection_default_on_save(self):
self.discipline.name = "Fallback Discipline"
self.discipline.save()

test_co = self.collectionobjects[0]
Collectionobject.objects.filter(pk=test_co.pk).update(
collectionobjecttype=None
)

test_co.refresh_from_db()
self.assertIsNone(test_co.collectionobjecttype)
self.assertIsNone(self.collection.collectionobjecttype)

test_co.save()
test_co.refresh_from_db()
self.collection.refresh_from_db()

self.assertIsNotNone(self.collection.collectionobjecttype)
self.assertEqual(
self.collection.collectionobjecttype,
test_co.collectionobjecttype
)
self.assertEqual(
test_co.collectionobjecttype.name,
"Fallback Discipline"
)
self.assertEqual(
test_co.collectionobjecttype.taxontreedef,
self.discipline.taxontreedef
)

def test_saving_determination_populates_existing_collectionobject_type(self):
self.discipline.name = "Fallback Discipline"
self.discipline.save()

test_co = self.collectionobjects[0]
Collection.objects.filter(pk=self.collection.pk).update(
collectionobjecttype=None
)
Collectionobject.objects.filter(pk=test_co.pk).update(
collectionobjecttype=None
)

test_co.refresh_from_db()
self.collection.refresh_from_db()
self.assertIsNone(test_co.collectionobjecttype)
self.assertIsNone(self.collection.collectionobjecttype)

test_co.determinations.create(remarks="new determination")
test_co.refresh_from_db()
self.collection.refresh_from_db()

self.assertIsNotNone(test_co.collectionobjecttype)
self.assertEqual(
self.collection.collectionobjecttype,
test_co.collectionobjecttype
)
self.assertEqual(
test_co.collectionobjecttype.name,
"Fallback Discipline"
)
self.assertEqual(
test_co.collectionobjecttype.taxontreedef,
self.discipline.taxontreedef
)

def test_saving_determination_uses_collection_default_type(self):
default_type = Collectionobjecttype.objects.create(
name="default type",
collection=self.collection,
taxontreedef=self.discipline.taxontreedef
)
self.collection.collectionobjecttype = default_type
self.collection.save()

test_co = self.collectionobjects[0]
Collectionobject.objects.filter(pk=test_co.pk).update(
collectionobjecttype=None
)

test_co.refresh_from_db()
self.assertIsNone(test_co.collectionobjecttype)

test_co.determinations.create(remarks="new determination")
test_co.refresh_from_db()

self.assertEqual(test_co.collectionobjecttype, default_type)
3 changes: 0 additions & 3 deletions specifyweb/backend/stored_queries/tests/tests_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,9 +841,6 @@ def test_sqlalchemy_model_errors(self):
]
},
"CollectionObject": {
"not_found": [
"projects"
],
"incorrect_direction": {
"cojo": [
"onetomany",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,64 @@ describe('Collection Object business rules', () => {
expect(result.current[0]).toStrictEqual([]);
});

test('CollectionObject -> determinations: Save is not blocked when collection object type is missing', async () => {
const collectionObject = getBaseCollectionObject();
collectionObject.set('collectionObjectType', null as never, {
silent: true,
});

const determination =
collectionObject.getDependentResource('determinations')?.models[0];

const { result } = renderHook(() =>
useSaveBlockers(determination, tables.Determination.getField('Taxon'))
);

await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});

expect(result.current[0]).toStrictEqual([]);
});

test('CollectionObject -> determinations: Missing collection object type clears invalid determination blockers', async () => {
const collectionObject = getBaseCollectionObject();
collectionObject.set(
'collectionObjectType',
getResourceApiUrl('CollectionObjectType', 1)
);

const determination =
collectionObject.getDependentResource('determinations')?.models[0];

const { result } = renderHook(() =>
useSaveBlockers(determination, tables.Determination.getField('Taxon'))
);

await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});
expect(result.current[0]).toStrictEqual([
resourcesText.invalidDeterminationTaxon(),
]);

collectionObject.set('collectionObjectType', null as never, {
silent: true,
});

await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});

expect(result.current[0]).toStrictEqual([]);
});

test('Newly added determinations are current by default', async () => {
const collectionObject = getBaseCollectionObject();
const determinations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ export const businessRuleDefs: MappedBusinessRuleDefs = {
determinations.models.map(async (det) => det.rgetPromise('taxon'))
);
const coType = await resource.rgetPromise('collectionObjectType');

if (coType === null) {
determinations.models.forEach((determination) => {
setSaveBlockers(
determination,
determination.specifyTable.field.taxon,
[],
DETERMINATION_TAXON_KEY
);
});
return;
}

const coTypeTreeDef = coType.get('taxonTreeDef');

// Block save when a Determination -> Taxon does not belong to the COType's tree definition
Expand Down
74 changes: 73 additions & 1 deletion specifyweb/specify/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,78 @@ def log_sqlalchemy_query(query):
# Run in the storred_queries.execute file, in the execute function, right before the return statement, line 546
# from specifyweb.specify.utils import log_sqlalchemy_query; log_sqlalchemy_query(query)

def get_or_create_default_collection_object_type(collection: spmodels.Collection, using: str = "default"):
db = using or "default"

collectionobjecttype = collection.collectionobjecttype

if collectionobjecttype is not None:
return collectionobjecttype

discipline_name = collection.discipline.name
taxon_tree_def_id = collection.discipline.taxontreedef_id

if discipline_name is None or taxon_tree_def_id is None:
logger.warning(
"Cannot create default Collectionobjecttype for collection %s: "
"discipline_name=%r, discipline.taxontreedef_id=%r, "
"collectionobjecttype=%r.",
collection.pk,
discipline_name,
taxon_tree_def_id,
collectionobjecttype,
)
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.

default_type, _ = spmodels.Collectionobjecttype.objects.using(db).get_or_create(
name=discipline_name,
collection=collection,
taxontreedef_id=taxon_tree_def_id,
)

type(collection).objects.using(db).filter(
pk=collection.pk,
collectionobjecttype__isnull=True,
).update(collectionobjecttype=default_type)
collection.collectionobjecttype = default_type

return default_type

def ensure_collection_object_type(
collection_object: spmodels.Collectionobject,
using: str = "default",
persist: bool = False,
):
db = using or "default"

if collection_object.collectionobjecttype is not None:
return collection_object.collectionobjecttype

if collection_object.collection.collectionobjecttype is not None:
collection_object.collectionobjecttype = (
collection_object.collection.collectionobjecttype
)
elif collection_object.pk is not None:
collection_object.collectionobjecttype = (
get_or_create_default_collection_object_type(
collection_object.collection, using=db
)
)

if (
persist
and collection_object.pk is not None
and collection_object.collectionobjecttype is not None
):
type(collection_object).objects.using(db).filter(
pk=collection_object.pk,
collectionobjecttype__isnull=True,
).update(
collectionobjecttype_id=collection_object.collectionobjecttype_id
)

return collection_object.collectionobjecttype

def create_default_collection_types(apps, using="default"):
db = using or "default"

Expand Down Expand Up @@ -90,4 +162,4 @@ def get_picklists(collection: spmodels.Collection, tablename: str, fieldname: st
if len(collection_picklists) > 0:
picklists = collection_picklists

return picklists, schemaitem
return picklists, schemaitem
Loading