From 06217dd3f168f7bf1e841ff998f5043e8d5755ba Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 08:51:51 -0500 Subject: [PATCH 01/11] [test] Add new chronostratigraphy --- .../tests/test_geologictimeperiod.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 75430a3652a..3f4f247a120 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -2,6 +2,8 @@ from django.db.models import ProtectedError from specifyweb.specify import models +from specifyweb.specify.api.crud import delete_resource, post_resource, update_obj +from specifyweb.specify.api.serializers import uri_for_model from specifyweb.specify.tests.test_api import ApiTests class Geologictimeperiod(ApiTests): @@ -14,6 +16,60 @@ def setUp(self): definitionitem=self.geologictimeperiodtreedef.treedefitems.create( name="root", rankid=0)) + self.era_rank = self.rootgtp.definitionitem.children.create( + name="Era", + rankid=100, + treedef=self.geologictimeperiodtreedef, + ) + + def _period_data(self, name, **overrides): + data = { + "name": name, + "parent": uri_for_model("geologictimeperiod", self.rootgtp.id), + "definitionitem": uri_for_model( + "geologictimeperiodtreedefitem", self.era_rank.id + ), + "remarks": "Test period", + "startperiod": 251.9, + "endperiod": 66.0, + "isbiostrat": True, + } + data.update(overrides) + return data + + def _create_period(self, name, **overrides): + return post_resource( + self.collection, + self.agent, + "geologictimeperiod", + self._period_data(name, **overrides), + ) + + def _create_existing_period(self, name): + return self.rootgtp.children.create( + name=name, + definition=self.geologictimeperiodtreedef, + definitionitem=self.era_rank, + rankid=self.era_rank.rankid, + remarks="Created before this version", + ) + + def test_add_geologictimeperiod(self): + period = self._create_period("Mesozoic") + + self.assertIsNotNone(period.id) + self.assertExists( + models.Geologictimeperiod.objects.filter( + id=period.id, + name="Mesozoic", + parent=self.rootgtp, + ) + ) + self.assertEqual(period.definition_id, self.geologictimeperiodtreedef.id) + self.assertEqual(period.definitionitem_id, self.era_rank.id) + self.assertEqual(period.rankid, self.era_rank.rankid) + + period.delete() def test_delete_blocked_by_biostratspaleocontext(self): self.rootgtp.biostratspaleocontext.create( From 28bf45056011482f7c248c3342c01ef007142999 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 08:52:00 -0500 Subject: [PATCH 02/11] [test] Clone chronostratigraphy --- .../tests/test_geologictimeperiod.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 3f4f247a120..6a1afbd8082 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -71,6 +71,38 @@ def test_add_geologictimeperiod(self): period.delete() + def test_clone_geologictimeperiod(self): + original = self._create_period( + "Paleozoic", + remarks="Original remarks", + startperiod=541.0, + endperiod=252.0, + isbiostrat=False, + ) + + clone = self._create_period( + original.name, + remarks=original.remarks, + startperiod=original.startperiod, + endperiod=original.endperiod, + isbiostrat=original.isbiostrat, + ) + + self.assertNotEqual(clone.id, original.id) + self.assertEqual(clone.name, original.name) + self.assertEqual(clone.parent_id, original.parent_id) + self.assertEqual(clone.definitionitem_id, original.definitionitem_id) + self.assertEqual(clone.remarks, original.remarks) + self.assertEqual(clone.startperiod, original.startperiod) + self.assertEqual(clone.endperiod, original.endperiod) + self.assertEqual(clone.isbiostrat, original.isbiostrat) + + original.refresh_from_db() + self.assertEqual(original.name, "Paleozoic") + + clone.delete() + original.delete() + def test_delete_blocked_by_biostratspaleocontext(self): self.rootgtp.biostratspaleocontext.create( discipline=self.discipline) From 5b8e27064d3ea61b1581a8423727b6c7a2650059 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 08:52:07 -0500 Subject: [PATCH 03/11] [test] Delete new chronostratigraphy --- .../tests/test_geologictimeperiod.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 6a1afbd8082..41270575050 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -103,6 +103,22 @@ def test_clone_geologictimeperiod(self): clone.delete() original.delete() + def test_delete_new_geologictimeperiod(self): + period = self._create_period("Triassic") + period_id = period.id + + delete_resource( + self.collection, + self.agent, + "geologictimeperiod", + period_id, + period.version, + ) + + self.assertNotExists( + models.Geologictimeperiod.objects.filter(id=period_id) + ) + def test_delete_blocked_by_biostratspaleocontext(self): self.rootgtp.biostratspaleocontext.create( discipline=self.discipline) From 98eb8a0c9ff2f7e1eb8f9aab263841b667c659a6 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 08:52:14 -0500 Subject: [PATCH 04/11] [test] Edit previous chronostratigraphy --- .../tests/test_geologictimeperiod.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 41270575050..e2234bd2920 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -119,6 +119,25 @@ def test_delete_new_geologictimeperiod(self): models.Geologictimeperiod.objects.filter(id=period_id) ) + def test_edit_geologictimeperiod_created_in_previous_version(self): + period = self._create_existing_period("Jurassic") + + update_obj( + self.collection, + self.agent, + "geologictimeperiod", + period.id, + period.version, + {"name": "Jurassic (updated)", "remarks": "Updated remarks"}, + ) + + period.refresh_from_db() + self.assertEqual(period.name, "Jurassic (updated)") + self.assertEqual(period.remarks, "Updated remarks") + self.assertEqual(period.version, 1) + + period.delete() + def test_delete_blocked_by_biostratspaleocontext(self): self.rootgtp.biostratspaleocontext.create( discipline=self.discipline) From 70fcab990c94be36ce84add1822ba3d31b8d9c19 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 08:52:23 -0500 Subject: [PATCH 05/11] [test] Delete previous chronostratigraphy --- .../tests/test_geologictimeperiod.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index e2234bd2920..effdcb53467 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -138,6 +138,22 @@ def test_edit_geologictimeperiod_created_in_previous_version(self): period.delete() + def test_delete_geologictimeperiod_created_in_previous_version(self): + period = self._create_existing_period("Cretaceous") + period_id = period.id + + delete_resource( + self.collection, + self.agent, + "geologictimeperiod", + period_id, + period.version, + ) + + self.assertNotExists( + models.Geologictimeperiod.objects.filter(id=period_id) + ) + def test_delete_blocked_by_biostratspaleocontext(self): self.rootgtp.biostratspaleocontext.create( discipline=self.discipline) From b1f4f2da72fdfbbe81ef3ec82145e3c87d86301e Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 09:08:03 -0500 Subject: [PATCH 06/11] [test] Validate chronostratigraphy parent and rank --- .../tests/test_geologictimeperiod.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index effdcb53467..eb16ab8bb8b 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -5,6 +5,7 @@ from specifyweb.specify.api.crud import delete_resource, post_resource, update_obj from specifyweb.specify.api.serializers import uri_for_model from specifyweb.specify.tests.test_api import ApiTests +from ..exceptions import TreeBusinessRuleException class Geologictimeperiod(ApiTests): def setUp(self): @@ -71,6 +72,28 @@ def test_add_geologictimeperiod(self): period.delete() + def test_geologictimeperiod_rank_must_be_below_parent(self): + period_rank = self.era_rank.children.create( + name="Period", + rankid=200, + treedef=self.geologictimeperiodtreedef, + ) + parent = self.rootgtp.children.create( + name="Cenozoic", + definition=self.geologictimeperiodtreedef, + definitionitem=period_rank, + rankid=period_rank.rankid, + ) + + with self.assertRaises(TreeBusinessRuleException): + models.Geologictimeperiod.objects.create( + name="Invalid child", + parent=parent, + definition=self.geologictimeperiodtreedef, + definitionitem=self.era_rank, + rankid=self.era_rank.rankid, + ) + def test_clone_geologictimeperiod(self): original = self._create_period( "Paleozoic", From dc03c4f98a0c8cb7aaecc758e4af90792b444796 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 09:08:09 -0500 Subject: [PATCH 07/11] [test] Save chronostratigraphy optional fields --- .../tests/test_geologictimeperiod.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index eb16ab8bb8b..8e569713a9e 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -1,4 +1,6 @@ from unittest import skip +from decimal import Decimal + from django.db.models import ProtectedError from specifyweb.specify import models @@ -94,6 +96,33 @@ def test_geologictimeperiod_rank_must_be_below_parent(self): rankid=self.era_rank.rankid, ) + def test_geologictimeperiod_optional_fields_round_trip(self): + period = self._create_period( + "Cenozoic", + remarks="Optional fields", + startperiod=66.0, + endperiod=23.03, + startuncertainty=0.5, + enduncertainty=0.2, + standard="ICS", + text1="Text one", + text2="Text two", + isbiostrat=False, + ) + + period.refresh_from_db() + self.assertEqual(period.remarks, "Optional fields") + self.assertEqual(period.startperiod, Decimal("66.0")) + self.assertEqual(period.endperiod, Decimal("23.03")) + self.assertEqual(period.startuncertainty, Decimal("0.5")) + self.assertEqual(period.enduncertainty, Decimal("0.2")) + self.assertEqual(period.standard, "ICS") + self.assertEqual(period.text1, "Text one") + self.assertEqual(period.text2, "Text two") + self.assertFalse(period.isbiostrat) + + period.delete() + def test_clone_geologictimeperiod(self): original = self._create_period( "Paleozoic", From 5cd3df8b710fce0b5a885c08744ad739e983212f Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 30 Oct 2026 09:08:36 -0500 Subject: [PATCH 08/11] [test] Open chronostratigraphy chart from clock icon --- .../Attachments/__tests__/ChronoChart.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/ChronoChart.test.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/ChronoChart.test.tsx index 268d670152b..62dc8582c72 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/ChronoChart.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/ChronoChart.test.tsx @@ -10,6 +10,21 @@ describe('ChronoChart', () => { expect(asFragment()).toMatchSnapshot(); }); + test('clock icon opens the chronostratigraphic chart', async () => { + const { getByRole, user } = mount( + + + + ); + + await user.click( + getByRole('button', { name: 'Chronostratigraphic Chart' }) + ); + + expect(getByRole('dialog')).toBeInTheDocument(); + expect(getByRole('img', { name: 'Chrono Chart' })).toBeInTheDocument(); + }); + test('dialog open and close', async () => { const { getAllByRole, getByRole, queryAllByRole, user } = mount( From 859c8893048ba376df501a6873fb958ad15cdc0c Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 2 Nov 2026 10:02:50 -0600 Subject: [PATCH 09/11] [test] Assert persisted chronostratigraphy fields --- .../backend/businessrules/tests/test_geologictimeperiod.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 8e569713a9e..1bc5c3d90cd 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -72,6 +72,12 @@ def test_add_geologictimeperiod(self): self.assertEqual(period.definitionitem_id, self.era_rank.id) self.assertEqual(period.rankid, self.era_rank.rankid) + period.refresh_from_db() + self.assertEqual(period.remarks, "Test period") + self.assertEqual(period.startperiod, Decimal("251.9")) + self.assertEqual(period.endperiod, Decimal("66.0")) + self.assertIs(period.isbiostrat, True) + period.delete() def test_geologictimeperiod_rank_must_be_below_parent(self): From c55b558771210d454ab1c3fa6355cd5215378b59 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 2 Nov 2026 10:02:57 -0600 Subject: [PATCH 10/11] [test] Assert cloned chronostratigraphy fields --- .../backend/businessrules/tests/test_geologictimeperiod.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 1bc5c3d90cd..1111f654835 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -155,6 +155,12 @@ def test_clone_geologictimeperiod(self): self.assertEqual(clone.endperiod, original.endperiod) self.assertEqual(clone.isbiostrat, original.isbiostrat) + clone.refresh_from_db() + self.assertEqual(clone.remarks, "Original remarks") + self.assertEqual(clone.startperiod, Decimal("541.0")) + self.assertEqual(clone.endperiod, Decimal("252.0")) + self.assertIs(clone.isbiostrat, False) + original.refresh_from_db() self.assertEqual(original.name, "Paleozoic") From bd6c21ec0efae552ff3cbdaaf385df077f5baa46 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 2 Nov 2026 10:03:06 -0600 Subject: [PATCH 11/11] [test] Edit chronostratigraphy with nonzero version --- .../tests/test_geologictimeperiod.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py index 1111f654835..c241b0ce573 100644 --- a/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py +++ b/specifyweb/backend/businessrules/tests/test_geologictimeperiod.py @@ -202,6 +202,26 @@ def test_edit_geologictimeperiod_created_in_previous_version(self): period.delete() + def test_edit_geologictimeperiod_with_nonzero_version(self): + period = self._create_existing_period("Triassic") + models.Geologictimeperiod.objects.filter(id=period.id).update(version=2) + period.refresh_from_db() + + update_obj( + self.collection, + self.agent, + "geologictimeperiod", + period.id, + period.version, + {"name": "Triassic (updated)"}, + ) + + period.refresh_from_db() + self.assertEqual(period.name, "Triassic (updated)") + self.assertEqual(period.version, 3) + + period.delete() + def test_delete_geologictimeperiod_created_in_previous_version(self): period = self._create_existing_period("Cretaceous") period_id = period.id