Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 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.

4 changes: 4 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 @@
self.earth.id,
"geographyid",
False,
False,
'all',
session,
)
expected = [
Expand Down Expand Up @@ -402,6 +404,8 @@
self.na.id,
"name",
False,
False,
'all',
session,
)
expected = [
Expand Down Expand Up @@ -492,14 +496,14 @@
yield session

with _run_for_row() as session:
results = get_tree_rows(
self.taxontreedef.id,
"Taxon",
root.id,
"name",
True,
session,
)

Check failure

Code scanning / CodeQL

Wrong number of arguments in a call Error test

Call to
function get_tree_rows
with too few arguments; should be no fewer than 8.
expected = [
(
animalia.id,
Expand Down
53 changes: 50 additions & 3 deletions specifyweb/backend/trees/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from specifyweb.specify import models as spmodels
from specifyweb.specify.api.crud import get_object_or_404
from specifyweb.specify.api.serializers import obj_to_data, toJson
from sqlalchemy import select, func, distinct, literal
from sqlalchemy import and_, select, func, distinct, literal

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'and_' is not used.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
from sqlalchemy.orm import aliased
from jsonschema import validate # type: ignore
from jsonschema.exceptions import ValidationError # type: ignore
Expand Down Expand Up @@ -165,13 +165,15 @@
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 @@
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 == None)

Check notice

Code scanning / CodeQL

Testing equality to None Note

Testing for None should use the 'is' operator.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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
25 changes: 19 additions & 6 deletions specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ export function QueryComboBox({
relatedTable,
subViewRelationship,
treeDefinition,
typeSearchName: typeSearch.name,
}),
})
)
Expand Down Expand Up @@ -547,6 +548,10 @@ export function QueryComboBox({
relatedTable,
subViewRelationship,
treeDefinition,
typeSearchName:
typeof typeSearch === 'object'
? typeSearch.name
: undefined,
})
.map(serializeResource)
.map(({ fieldName, startValue }) =>
Expand Down Expand Up @@ -587,13 +592,21 @@ export function QueryComboBox({
isNot: false,
value: startValue,
}
: f.error(
`extended filter not created`,
{
fieldName,
startValue,
: fieldName === 'isBioStrat'
? {
field: 'isBioStrat',
isRelationship: false,
operation: 'in',
isNot: false,
value: '1',
}
)
: f.error(
`extended filter not created`,
{
fieldName,
startValue,
}
)
)
),
})
Expand Down
Loading
Loading