Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f6cd2cf
feat(trees): sort chronostrat by period
grantfitzsimmons Jul 3, 2026
650af31
feat: add ranges to chronostrat
grantfitzsimmons Jul 3, 2026
8e7229b
fix: minor localization improvement
grantfitzsimmons Jul 3, 2026
dd261a5
feat(trees): make the start and end period smaller
grantfitzsimmons Jul 3, 2026
7d0a3c6
fix: add geologic time period labels
grantfitzsimmons Jul 3, 2026
0c5d1b4
fix(trees): hide uncertainty where there is none
grantfitzsimmons Jul 3, 2026
32b81ac
Lint code with ESLint and Prettier
grantfitzsimmons Jul 3, 2026
c996d2f
fix: prevent localization stripping
grantfitzsimmons Jul 3, 2026
874f962
refactor(trees): simplify label construction
grantfitzsimmons Jul 3, 2026
21fe094
fix(tests): add new parameter
grantfitzsimmons Jul 3, 2026
241ad64
Merge branch 'issue-6049' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 3, 2026
137f7a5
Lint code with ESLint and Prettier
grantfitzsimmons Jul 3, 2026
264c0c4
feat(trees): add biostrat filter to chronostrat
grantfitzsimmons Jul 4, 2026
373a144
feat: remove weird capitalization
grantfitzsimmons Jul 4, 2026
4e5774b
feat(trees): make `biostrat` search only biostrat records
grantfitzsimmons Jul 4, 2026
3d1d576
fix(QueryComboBox): filter BioStrat properly
grantfitzsimmons Jul 4, 2026
20a7100
Lint code with ESLint and Prettier
grantfitzsimmons Jul 4, 2026
897fb1b
feat(trees): add biostrat search filtering
grantfitzsimmons Jul 4, 2026
87ab9a2
fix(QueryComboBox): fix search in qcbx dialog
grantfitzsimmons Jul 4, 2026
21a40a1
Merge branch 'issue-8256' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 4, 2026
90f0ed9
Lint code with ESLint and Prettier
grantfitzsimmons Jul 4, 2026
41b8c02
feat(tests): add BioStrat filter tests
grantfitzsimmons Jul 4, 2026
7d49186
Merge branch 'issue-8256' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 4, 2026
7bc61a2
Lint code with ESLint and Prettier
grantfitzsimmons Jul 4, 2026
8ca11a3
fix(trees): make sure arrows appear correctly
grantfitzsimmons Jul 4, 2026
1d289f3
Merge branch 'issue-8256' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 4, 2026
15fa14b
fix(trees): filter tree descendants by biostrat
grantfitzsimmons Jul 4, 2026
56be873
Merge pull request #8257 from specify/issue-8256
grantfitzsimmons Jul 7, 2026
2552c74
Lint code with ESLint and Prettier
grantfitzsimmons Jul 7, 2026
f87e791
Merge origin/main into issue-6049 and resolve trees conflict
Copilot Jul 7, 2026
4e666b7
Merge remote-tracking branch 'origin/issue-6049' into issue-6049
Copilot Jul 7, 2026
1910390
Update specifyweb/frontend/js_src/lib/components/TreeView/index.tsx
grantfitzsimmons Jul 7, 2026
0bcf1fd
fix: remove unused import
grantfitzsimmons Jul 7, 2026
9705c73
Test: Fix backend tree tests
Jul 8, 2026
b06e6c5
Merge branch 'main' into issue-6049
CarolineDenis Jul 8, 2026
ea43a4e
Fix: Fix localization
Jul 8, 2026
d352b11
Potential fix for pull request finding 'CodeQL / Testing equality to …
CarolineDenis Jul 8, 2026
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
2 changes: 1 addition & 1 deletion config/common/schema_localization_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,7 @@
"guid": {"name": "GUID", "desc": "GUID", "format": null, "ishidden": true, "isuiformatter": false, "picklistname": null, "type": "java.lang.String", "isrequired": false, "weblinkname": null},
"highestchildnodenumber": {"name": "Highest Child Node Number", "desc": "Highest Child Node Number", "format": null, "ishidden": true, "isuiformatter": false, "picklistname": null, "type": "java.lang.Integer", "isrequired": false, "weblinkname": null},
"isaccepted": {"name": "Is Preferred", "desc": "Is Preferred", "format": null, "ishidden": true, "isuiformatter": false, "picklistname": null, "type": "java.lang.Boolean", "isrequired": false, "weblinkname": null},
"isbiostrat": {"name": "Is BioStratigraphy", "desc": "Is BioStratigraphy", "format": null, "ishidden": false, "isuiformatter": false, "picklistname": null, "type": null, "isrequired": false, "weblinkname": null},
"isbiostrat": {"name": "Is Biostratigraphy", "desc": "Is Biostratigraphy", "format": null, "ishidden": false, "isuiformatter": false, "picklistname": null, "type": null, "isrequired": false, "weblinkname": null},
"modifiedbyagent": {"name": "Modified By Agent", "desc": "Modified By Agent", "format": null, "ishidden": true, "isuiformatter": false, "picklistname": null, "type": null, "isrequired": false, "weblinkname": null},
"name": {"name": "Name", "desc": "Name", "format": null, "ishidden": false, "isuiformatter": false, "picklistname": null, "type": "java.lang.String", "isrequired": false, "weblinkname": null},
"nodenumber": {"name": "Node Number", "desc": "Node Number", "format": null, "ishidden": true, "isuiformatter": false, "picklistname": null, "type": "java.lang.Integer", "isrequired": false, "weblinkname": null},
Expand Down
2 changes: 1 addition & 1 deletion specifyweb/backend/context/data/schemalocalization.json

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions specifyweb/backend/trees/tests/test_trees.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ def _run_for_row():
self.earth.id,
"geographyid",
False,
False,
'all',
session,
)
expected = [
Expand All @@ -362,8 +364,13 @@ def _run_for_row():
None,
None,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
self.na.children.count(),
"NA Syn 0, NA Syn 1",
"NULL",
),
(
na_syn_0.id,
Expand All @@ -375,8 +382,13 @@ def _run_for_row():
self.na.id,
self.na.fullname,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
na_syn_0.children.count(),
None,
"NULL",
),
(
na_syn_1.id,
Expand All @@ -388,8 +400,13 @@ def _run_for_row():
self.na.id,
self.na.fullname,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
na_syn_1.children.count(),
None,
"NULL",
),
]

Expand All @@ -402,6 +419,8 @@ def _run_for_row():
self.na.id,
"name",
False,
False,
'all',
session,
)
expected = [
Expand All @@ -415,8 +434,13 @@ def _run_for_row():
None,
None,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
self.usa.children.count(),
"USA Syn 0, USA Syn 1, USA Syn 2",
"NULL",
),
(
usa_syn_0.id,
Expand All @@ -428,8 +452,13 @@ def _run_for_row():
self.usa.id,
self.usa.fullname,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
0,
None,
"NULL",
),
(
usa_syn_1.id,
Expand All @@ -441,8 +470,13 @@ def _run_for_row():
self.usa.id,
self.usa.fullname,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
0,
None,
"NULL",
),
(
usa_syn_2.id,
Expand All @@ -454,8 +488,13 @@ def _run_for_row():
self.usa.id,
self.usa.fullname,
"NULL",
"NULL",
"NULL",
"NULL",
"NULL",
0,
None,
"NULL",
),
]
self.assertCountEqual(results, expected)
Expand Down Expand Up @@ -498,6 +537,8 @@ def _run_for_row():
root.id,
"name",
True,
False,
'all',
session,
)
expected = [
Expand All @@ -511,8 +552,13 @@ def _run_for_row():
None,
None,
animalia.author,
"NULL",
"NULL",
"NULL",
"NULL",
0,
metazoa.fullname,
"NULL",
),
(
metazoa.id,
Expand All @@ -524,8 +570,13 @@ def _run_for_row():
animalia.id,
animalia.fullname,
metazoa.author,
"NULL",
"NULL",
"NULL",
"NULL",
0,
None,
"NULL",
),
]

Expand Down
51 changes: 49 additions & 2 deletions specifyweb/backend/trees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,15 @@ def tree_view(request, treedef, tree: TREE_TABLE, parentid, sortfield):
implementation/design decisions
"""
include_author = request.GET.get('includeauthor', False) and tree == 'taxon'
include_start_end_periods = request.GET.get('includestartendperiods', False) and tree == 'geologictimeperiod'
Comment thread
grantfitzsimmons marked this conversation as resolved.
biostrat = request.GET.get('biostrat', 'all')
with sqlmodels.session_context() as session:
set_group_concat_max_len(session.connection())
results = get_tree_rows(treedef, tree, parentid, sortfield, include_author, session)
results = get_tree_rows(treedef, tree, parentid, sortfield, include_author, include_start_end_periods, biostrat, session)
return HttpResponse(toJson(results), content_type='application/json')


def get_tree_rows(treedef, tree, parentid, sortfield, include_author, session):
def get_tree_rows(treedef, tree, parentid, sortfield, include_author, include_start_end_periods, biostrat, session):
tree_table = spmodels.datamodel.get_table(tree)
parentid = None if parentid == 'null' else int(parentid)

Expand Down Expand Up @@ -210,10 +212,55 @@ def get_tree_rows(treedef, tree, parentid, sortfield, include_author, session):
node.AcceptedID.label("accepted_id"),
accepted.fullName.label("accepted_fullname"),
(node.author if include_author else literal("NULL")).label("author"),
(
node.startPeriod
if include_start_end_periods
else literal("NULL")
).label("start_period"),
(
node.startUncertainty
if include_start_end_periods
else literal("NULL")
).label("start_uncertainty"),
(
node.endPeriod
if include_start_end_periods
else literal("NULL")
).label("end_period"),
(
node.endUncertainty
if include_start_end_periods
else literal("NULL")
).label("end_uncertainty"),
func.coalesce(child_counts.c.child_count, 0).label("child_count"),
synonym_names.c.synonyms.label("synonyms"),
(
node.isBioStrat
if tree == 'geologictimeperiod'
else literal("NULL")
).label("is_biostrat"),
]

if biostrat != 'all' and tree == 'geologictimeperiod':
# Count all matching descendants (any depth) using nested-set ranges
if biostrat == 'bio':
descendant_match = child.isBioStrat == True
else:
descendant_match = (child.isBioStrat == False) | (child.isBioStrat.is_(None))
matching_descendant_count = (
select(func.count(distinct(child._id)))
.where(
child.nodeNumber > node.nodeNumber,
child.nodeNumber <= node.highestChildNodeNumber,
descendant_match,
getattr(child, tree_table.name + "TreeDefID") == int(treedef),
)
.correlate(node)
.scalar_subquery()
.label("matching_descendant_count")
)
cols.append(matching_descendant_count)

query = (
select(*cols)
.outerjoin(accepted, node.AcceptedID == accepted._id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,13 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
displayChronoPeriods: definePref<boolean>({
title: preferencesText.displayChronoPeriods(),
requiresReload: false,
visible: true,
defaultValue: true,
type: 'java.lang.Boolean',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { requireContext } from '../../../tests/helpers';
import { serializeResource } from '../../DataModel/serializers';
import { tables } from '../../DataModel/tables';
import { queryFieldFilterSpecs } from '../../QueryBuilder/FieldFilterSpec';
import { getQueryComboBoxConditions } from '../helpers';

requireContext();

describe('getQueryComboBoxConditions with BioStrat typeSearchName', () => {
const getBaseParams = () => ({
// Use a non-tree resource to avoid toTreeTable() requiring tree data
resource: new tables.CollectionObject.Resource(
{},
{ noBusinessRules: true }
),
fieldName: 'name' as const,
treeData: undefined,
collectionRelationships: undefined,
relatedTable: tables.GeologicTimePeriod,
subViewRelationship: undefined,
treeDefinition: undefined,
});

test('adds isBioStrat=true filter when typeSearchName is BioStrat', () => {
const conditions = getQueryComboBoxConditions({
...getBaseParams(),
typeSearchName: 'BioStrat',
});
const serialized = conditions.map(serializeResource);
expect(serialized).toContainEqual(
expect.objectContaining({
fieldName: 'isBioStrat',
startValue: 'true',
operStart: queryFieldFilterSpecs.true.id,
})
);
});

test('does NOT add isBioStrat filter when typeSearchName is something else', () => {
const conditions = getQueryComboBoxConditions({
...getBaseParams(),
typeSearchName: 'ChronosStrat',
});
const serialized = conditions.map(serializeResource);
expect(
serialized.filter(({ fieldName }) => fieldName === 'isBioStrat')
).toHaveLength(0);
});

test('does NOT add isBioStrat filter when typeSearchName is undefined', () => {
const conditions = getQueryComboBoxConditions({
...getBaseParams(),
});
const serialized = conditions.map(serializeResource);
expect(
serialized.filter(({ fieldName }) => fieldName === 'isBioStrat')
).toHaveLength(0);
});

test('does NOT add isBioStrat filter for non-GeologicTimePeriod tables', () => {
const conditions = getQueryComboBoxConditions({
...getBaseParams(),
relatedTable: tables.Taxon,
typeSearchName: 'BioStrat',
});
const serialized = conditions.map(serializeResource);
expect(
serialized.filter(({ fieldName }) => fieldName === 'isBioStrat')
).toHaveLength(0);
});
});
18 changes: 18 additions & 0 deletions specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function getQueryComboBoxConditions({
subViewRelationship,
relatedTable,
treeDefinition,
typeSearchName,
}: {
readonly resource: SpecifyResource<AnySchema>;
readonly fieldName: string;
Expand All @@ -89,6 +90,7 @@ export function getQueryComboBoxConditions({
readonly relatedTable: SpecifyTable;
readonly subViewRelationship: Relationship | undefined;
readonly treeDefinition: string | undefined;
readonly typeSearchName?: string | undefined;
}): RA<SpecifyResource<SpQueryField>> {
const fields: WritableArray<SpecifyResource<SpQueryField>> = [];
const treeResource = toTreeTable(resource);
Expand Down Expand Up @@ -164,6 +166,22 @@ export function getQueryComboBoxConditions({
);
}

/**
* When looking up BioStrat values for PaleoContext, only show
* GeologicTimePeriod nodes where isBioStrat is TRUE.
*/
if (
typeSearchName === 'BioStrat' &&
relatedTable === tables.GeologicTimePeriod
)
fields.push(
QueryFieldSpec.fromPath(tables.GeologicTimePeriod.name, ['isBioStrat'])
.toSpQueryField()
.set('isDisplay', false)
.set('operStart', queryFieldFilterSpecs.true.id)
.set('startValue', 'true')
);

if (
typeof collectionRelationships === 'object' &&
fieldName === 'collectionRelType'
Expand Down
Loading
Loading