Skip to content

Fix CollectionObject business rule crash on missing CollectionObjectType#7905

Open
acwhite211 wants to merge 7 commits into
mainfrom
issue-7870
Open

Fix CollectionObject business rule crash on missing CollectionObjectType#7905
acwhite211 wants to merge 7 commits into
mainfrom
issue-7870

Conversation

@acwhite211
Copy link
Copy Markdown
Member

@acwhite211 acwhite211 commented Apr 7, 2026

Fixes #7870

Saving a CollectionObject with a missing collectionObjectType could trigger a front-end business rule crash and leave the record stuck on the "Loading..." dialog instead of completing the save. This affected older records, including records created in Specify 6, where CollectionObjectTypeID may still be null.

Added a null guard in the CollectionObject collectionObjectType business rule and skip determination taxon tree validation when no collection object type is set. Clear any existing determination taxon save blockers when collectionObjectType is missing. Also added unit tests for saving when collectionObjectType is null and clearing invalid determination blockers when collectionObjectType becomes null

On the back-end, I decided to go ahead and opportunistically normalize legacy null collectionObjectType values when an existing CollectionObject is saved in Specify 7. If the collection already has a default Collection Object Type, that value is applied on save. If both the record and the collection default are null, Specify creates or reuses the collection’s discipline-based default type, assigns it to the collection, and uses it for the saved record. I'm ok with reverting this section if anyone thinks we should leave them as null.

Checklist

  • Self-review the PR after opening it to make sure the changes look good and
    self-explanatory (or properly documented)
  • Add relevant issue to release milestone
  • Add pr to documentation list
  • Add automated tests

Testing instructions

  • Open a database that contains at least one CollectionObject with no Collection Object Type set (CollectionObjectTypeID = null). Older Specify 6-created records are the main target.
  • Use the CO QB to find a CO record without a COT. If you can't find one, might need to go into the database to set one of them to null. Might be able to set a COT to null in batch edit.
image
  • In Data Entry, open one of those records and confirm the Collection Object Type field is blank.
  • Add a new Determination to the record.
  • Click Save.
  • Verify the save completes normally instead of hanging on the "Loading..." dialog.
  • Verify the page remains usable after save and no infinite loading state appears.
  • Refresh or reopen the record and confirm the new determination was actually saved.
  • Run the same CO QB query to make sure that the COT is not null anymore.
image

Summary by CodeRabbit

  • Bug Fixes

    • Collection object types are now automatically populated when creating determinations.
    • Improved robustness when saving records without an explicit collection object type.
    • Enhanced validation to properly handle null collection object types in dependent determinations.
  • Tests

    • Added comprehensive test coverage for collection object type defaulting scenarios.
    • Added validation tests for determination creation with various collection object type states.

Review Change Stack

@acwhite211 acwhite211 changed the title Fix CollectionObject business rule crash on missing collectionObjectType Fix CollectionObject business rule crash on missing CollectionObjectType Apr 8, 2026
@acwhite211 acwhite211 marked this pull request as ready for review April 8, 2026 19:18
@acwhite211 acwhite211 requested review from a team and grantfitzsimmons April 8, 2026 19:18
Copy link
Copy Markdown
Collaborator

@bhumikaguptaa bhumikaguptaa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Verify the save completes normally instead of hanging on the "Loading..." dialog.
  • Verify the page remains usable after save and no infinite loading state appears.
  • Refresh or reopen the record and confirm the new determination was actually saved.
  • Run the same CO QB query to make sure that the COT is not null anymore.

Everything looks good, except when i refresh the page and rerun the query, the COT is still null.
Db:https://kuentoissue7870-issue-7870.test.specifysystems.org/specify/query/new/collectionobject/

@github-project-automation github-project-automation Bot moved this from 📋Back Log to Dev Attention Needed in General Tester Board Apr 8, 2026
@grantfitzsimmons grantfitzsimmons modified the milestones: 7.12.1, 7.12.2 Apr 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR adds automatic defaulting of CollectionObject collection object type, resolving a hang that occurs when saving records without an assigned type. It introduces utility functions to derive and persist defaults from collection metadata, integrates them into pre-save hooks, and adds frontend null-guard logic to prevent errors.

Changes

CollectionObject Type Defaulting

Layer / File(s) Summary
Core utility functions
specifyweb/specify/api/utils.py
get_or_create_default_collection_object_type derives a default type from collection discipline and taxon tree, creates/fetches it via ORM, and updates null collection rows. ensure_collection_object_type resolves a CollectionObject's type by checking existing value, copying from collection, or deriving a default; optionally persists the type with conditional ORM updates. get_picklists return statement fixed to include computed picklists.
Backend business rule integration
specifyweb/backend/businessrules/rules/collectionobject_rules.py, specifyweb/backend/businessrules/rules/determination_rules.py
CollectionObject and Determination pre-save hooks call ensure_collection_object_type to resolve missing types before executing remaining rule logic, using active database alias and persist flag. Error message formatting adjusted.
Backend test coverage
specifyweb/backend/businessrules/tests/test_collectionobject.py
Five new tests verify type defaulting: new objects without collection defaults keep null type; existing objects use collection-configured defaults when cleared; fallback discipline-based defaults are created when collection has none; and determining a typeless object populates its type using collection or fallback defaults.
Frontend null-guard
specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts
CollectionObject.collectionObjectType field check adds early null-guard: when type is null, save blockers are set on dependent determination taxon fields and logic returns before accessing type properties, preventing TypeError.
Frontend test coverage
specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts
Two new tests verify that null collectionObjectType does not trigger determination save blockers and that previously-invalid blockers clear when type becomes null.

Suggested reviewers

  • emenslin
  • bhumikaguptaa
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Testing Instructions ⚠️ Warning Manual testing instructions are missing from the PR despite comprehensive automated tests being present. The PR template's testing instructions section is empty. Document manual testing steps: locate/create CollectionObject with null collectionobjecttype, add Determination, save, verify save completes and COT is populated.
Out of Scope Changes check ❓ Inconclusive The PR includes opportunistic back-end normalization of legacy null CollectionObjectType values beyond the minimal fix required, though the author acknowledges this can be reverted. Clarify whether the back-end normalization logic in utils.py and determination_rules.py is intentional scope or should be reverted to keep changes minimal to the crash fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: handling a crash when CollectionObjectType is missing by adding null guards in the business rule.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #7870: adds null guard in front-end business rule to prevent TypeError, clears invalid determination blockers, and includes unit tests for null CollectionObjectType scenarios.
Automatic Tests ✅ Passed PR includes comprehensive automatic tests: 5 backend tests for null collectionObjectType handling and default creation; 2 frontend tests for null-guard and blocker clearing behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7870

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@acwhite211 acwhite211 requested a review from bhumikaguptaa May 21, 2026 19:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
specifyweb/backend/businessrules/tests/test_collectionobject.py (1)

62-91: 💤 Low value

Small note on the discipline rename.

Renaming the discipline to "Fallback Discipline" before triggering the helper looks like a workaround for get_or_create(name=discipline.name, ...) colliding with a Collectionobjecttype that the test fixture already created under the original discipline name. The test passes, but it makes the intent a bit opaque — a one-line comment ("rename so the fallback get_or_create actually creates a new COT") would help the next person who touches this.

Also consider asserting on Collectionobjecttype.objects.filter(name="Fallback Discipline", collection=self.collection).count() == 1 to lock in "creates exactly one" behavior and guard against a future regression that double-creates under the race noted in utils.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specifyweb/backend/businessrules/tests/test_collectionobject.py` around lines
62 - 91, In
test_existing_collectionobject_without_type_creates_collection_default_on_save:
add a one-line comment explaining why the discipline name is renamed to
"Fallback Discipline" (so get_or_create(name=discipline.name, ...) will create a
new Collectionobjecttype instead of colliding with the fixture), and after
saving and refreshing assert that exactly one Collectionobjecttype exists for
that name and collection (e.g. assert
Collectionobjecttype.objects.filter(name="Fallback Discipline",
collection=self.collection).count() == 1) to guard against duplicate creation;
reference the test method name, the Collectionobjecttype model and the
get_or_create logic in utils.py when making the change.
specifyweb/specify/api/utils.py (1)

65-91: 💤 Low value

Logic for ensure_collection_object_type reads cleanly.

The branching (already-set → collection default → derive-from-discipline) matches the intended fallback chain, and gating the persist ORM update on pk is not None + collectionobjecttype__isnull=True is the right defensive shape so we never clobber a value that another writer has set in the meantime. Nice.

One small polish: when collection_object.pk is None and the collection has no default, the function returns None and the caller relies on Django's normal save path to write the (still-null) field. That's correct given the frontend now null-guards, but a one-line docstring spelling out the three branches and the "may return None" contract would save the next reader a trip through the call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specifyweb/specify/api/utils.py` around lines 65 - 91, Add a one-line
docstring to the ensure_collection_object_type function explaining its
three-branch behavior and return contract: if
collection_object.collectionobjecttype is already set return it; otherwise use
collection.collectionobjecttype as a default; otherwise derive via
get_or_create_default_collection_object_type; the function may return None when
no type can be determined and callers should handle that (persist and pk gating
behavior remains unchanged). Reference ensure_collection_object_type,
collection_object, persist, and get_or_create_default_collection_object_type in
the docstring for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@specifyweb/specify/api/utils.py`:
- Around line 38-42: The helper that reads collection.discipline (where
discipline_name = collection.discipline.name and taxon_tree_def_id =
collection.discipline.taxontreedef_id) currently returns None silently when
taxon_tree_def_id is null; update that helper in specify/api/utils.py to emit a
visible log warning (e.g., logger.warning) mentioning the collection identifier
and that discipline.taxontreedef_id is missing before returning None so the
missing COT/backfill state is observable; reference the discipline_name,
taxon_tree_def_id and collectionobjecttype variables in the message to aid
debugging.

---

Nitpick comments:
In `@specifyweb/backend/businessrules/tests/test_collectionobject.py`:
- Around line 62-91: In
test_existing_collectionobject_without_type_creates_collection_default_on_save:
add a one-line comment explaining why the discipline name is renamed to
"Fallback Discipline" (so get_or_create(name=discipline.name, ...) will create a
new Collectionobjecttype instead of colliding with the fixture), and after
saving and refreshing assert that exactly one Collectionobjecttype exists for
that name and collection (e.g. assert
Collectionobjecttype.objects.filter(name="Fallback Discipline",
collection=self.collection).count() == 1) to guard against duplicate creation;
reference the test method name, the Collectionobjecttype model and the
get_or_create logic in utils.py when making the change.

In `@specifyweb/specify/api/utils.py`:
- Around line 65-91: Add a one-line docstring to the
ensure_collection_object_type function explaining its three-branch behavior and
return contract: if collection_object.collectionobjecttype is already set return
it; otherwise use collection.collectionobjecttype as a default; otherwise derive
via get_or_create_default_collection_object_type; the function may return None
when no type can be determined and callers should handle that (persist and pk
gating behavior remains unchanged). Reference ensure_collection_object_type,
collection_object, persist, and get_or_create_default_collection_object_type in
the docstring for clarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 953818a5-0b81-4763-b70f-9154865b16cc

📥 Commits

Reviewing files that changed from the base of the PR and between 66b1266 and fa9ac44.

📒 Files selected for processing (6)
  • specifyweb/backend/businessrules/rules/collectionobject_rules.py
  • specifyweb/backend/businessrules/rules/determination_rules.py
  • specifyweb/backend/businessrules/tests/test_collectionobject.py
  • specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts
  • specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts
  • specifyweb/specify/api/utils.py

Comment on lines +38 to +42
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:
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent no-op when taxontreedef_id is missing.

discipline.name is a required field and unlikely to be None, but discipline.taxontreedef_id can legitimately be null for some legacy disciplines. When that happens this helper returns None without any signal, the caller silently leaves collectionobjecttype unset, and the pre-save hook completes "successfully" — which puts us right back in the original "save proceeds with null COT" territory that the frontend null-guard now has to catch.

Consider at least a logger.warning(...) so this state is observable in server logs when the backend backfill can't run.

📝 Suggested logging
     if discipline_name is None or taxon_tree_def_id is None:
+        logger.warning(
+            "Cannot create default Collectionobjecttype for collection %s: "
+            "discipline name or taxontreedef is missing.",
+            collection.pk,
+        )
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specifyweb/specify/api/utils.py` around lines 38 - 42, The helper that reads
collection.discipline (where discipline_name = collection.discipline.name and
taxon_tree_def_id = collection.discipline.taxontreedef_id) currently returns
None silently when taxon_tree_def_id is null; update that helper in
specify/api/utils.py to emit a visible log warning (e.g., logger.warning)
mentioning the collection identifier and that discipline.taxontreedef_id is
missing before returning None so the missing COT/backfill state is observable;
reference the discipline_name, taxon_tree_def_id and collectionobjecttype
variables in the message to aid debugging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Dev Attention Needed

Development

Successfully merging this pull request may close these issues.

Saving a record without a COT set will result in hanging rather than save

3 participants