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
60 changes: 36 additions & 24 deletions specifyweb/backend/stored_queries/query_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from collections import namedtuple, deque

from sqlalchemy import orm, sql, or_
from sqlalchemy import inspect
from sqlalchemy.orm.util import AliasedClass
from sqlalchemy.inspection import inspect as sa_inspect

import specifyweb.specify.models as spmodels
from specifyweb.backend.trees.utils import get_treedefs
Expand Down Expand Up @@ -29,47 +32,55 @@ def __new__(cls, *args, **kwargs):

def handle_tree_field(self, node, table, tree_rank: TreeRankQuery, next_join_path, current_field_spec: QueryFieldSpec):
query = self
if query.collection is None: raise AssertionError( # Not sure it makes sense to query across collections
f"No Collection found in Query for {table}",
{"table" : table,
"localizationKey" : "noCollectionInQuery"})
logger.info('handling treefield %s rank: %s field: %s', table, tree_rank.name, next_join_path)
if query.collection is None:
raise AssertionError(
f"No Collection found in Query for {table}",
{"table": table, "localizationKey": "noCollectionInQuery"},
)
logger.info("handling treefield %s rank: %s field: %s", table, tree_rank.name, next_join_path)

treedefitem_column = table.name + 'TreeDefItemID'
treedef_column = table.name + 'TreeDefID'

if (table, 'TreeRanks') in query.join_cache:
logger.debug("using join cache for %r tree ranks.", table)
ancestors, treedefs = query.join_cache[(table, 'TreeRanks')]
# Determine starting anchor correctly:
# If node is already an alias (from a relationship path), use that alias.
# If node is the mapped class (base table), don't alias it, start from the base table.
is_alias = isinstance(node, AliasedClass)
start_alias = node # keep as-is
mapped_cls = sa_inspect(node).mapper.class_ if is_alias else node

# Use the specific start anchor in the cache key, so each branch has its own chain
cache_key = (start_alias, "TreeRanks")

if cache_key in query.join_cache:
logger.debug("using join cache for %r tree ranks.", start_alias)
ancestors, treedefs = query.join_cache[cache_key]
else:

treedefs = get_treedefs(query.collection, table.name)

# We need to take the max here. Otherwise, it is possible that the same rank
# name may not occur at the same level across tree defs.
max_depth = max(depth for _, depth in treedefs)

ancestors = [node]
for _ in range(max_depth-1):
ancestor = orm.aliased(node)

# Start ancestry from the provided alias (e.g., HostTaxon alias)
ancestors = [start_alias]

# Build parent chain using aliases of the mapped class
for _ in range(max_depth - 1):
ancestor = orm.aliased(mapped_cls)
query = query.outerjoin(ancestor, ancestors[-1].ParentID == ancestor._id)
ancestors.append(ancestor)


logger.debug("adding to join cache for %r tree ranks.", table)
logger.debug("adding to join cache for %r tree ranks.", start_alias)
query = query._replace(join_cache=query.join_cache.copy())
query.join_cache[(table, 'TreeRanks')] = (ancestors, treedefs)
query.join_cache[cache_key] = (ancestors, treedefs)

item_model = getattr(spmodels, table.django_name + "treedefitem")

# TODO: optimize out the ranks that appear? cache them
treedefs_with_ranks: list[tuple[int, int]] = [tup for tup in [
(treedef_id, _safe_filter(item_model.objects.filter(treedef_id=treedef_id, name=tree_rank.name).values_list('id', flat=True)))
treedefs_with_ranks: list[tuple[int, int]] = [
(treedef_id, _safe_filter(item_model.objects.filter(treedef_id=treedef_id, name=tree_rank.name).values_list("id", flat=True)))
for treedef_id, _ in treedefs
# For constructing tree queries for batch edit
if (tree_rank.treedef_id is None or tree_rank.treedef_id == treedef_id)
] if tup[1] is not None]

]
assert len(treedefs_with_ranks) >= 1, "Didn't find the tree rank across any tree"

treedefitem_params = [treedefitem_id for (_, treedefitem_id) in treedefs_with_ranks]
Expand All @@ -96,7 +107,8 @@ def make_tree_field_spec(tree_node):
# We don't want to include treedef if the rank is not present.
new_filters = [
*query.internal_filters,
or_(getattr(node, treedef_column).in_(defs_to_filter_on), getattr(node, treedef_column) == None)]
or_(getattr(start_alias, treedef_column).in_(defs_to_filter_on), getattr(start_alias, treedef_column) == None),
]
query = query._replace(internal_filters=new_filters)

return query, column, field, table
Expand Down
19 changes: 14 additions & 5 deletions specifyweb/backend/stored_queries/queryfieldspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,17 +480,26 @@ def add_spec_to_query(
cycle_detector,
)
else:
query, orm_model, table, field = self.build_join(query, self.join_path)
if isinstance(field, TreeRankQuery):
tree_rank_idx = self.join_path.index(field)
tree_rank_idxs = [i for i, n in enumerate(self.join_path) if isinstance(n, TreeRankQuery)]
if tree_rank_idxs:
tree_rank_idx = tree_rank_idxs[0]
prefix = self.join_path[:tree_rank_idx] # up to (but not including) the tree-rank node
tree_rank_node = self.join_path[tree_rank_idx]
suffix = self.join_path[tree_rank_idx + 1 :] # field after the rank, e.g., "Name"

# Join only the prefix to obtain the correct starting alias (e.g., HostTaxon)
query, orm_model, table, _ = self.build_join(query, prefix)

# Build the CASE/joins for the tree rank starting at that alias
query, orm_field, field, table = query.handle_tree_field(
orm_model,
table,
field,
self.join_path[tree_rank_idx + 1 :],
tree_rank_node,
suffix,
self,
)
else:
query, orm_model, table, field = self.build_join(query, self.join_path)
try:
field_name = self.get_field().name
orm_field = getattr(orm_model, field_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Input, Label } from '../Atoms/Form';
import { icons } from '../Atoms/Icons';
import { Link } from '../Atoms/Link';
import { Dialog } from '../Molecules/Dialog';
import { hasPermission } from '../Permissions/helpers';
import {
allAppResources,
countAppResources,
Expand All @@ -23,7 +24,6 @@ import {
} from './filtersHelpers';
import type { AppResources } from './hooks';
import { appResourceSubTypes, appResourceTypes } from './types';
import { hasPermission } from '../Permissions/helpers';

export function AppResourcesFilters({
initialResources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import { loadingBar } from '../Molecules';
import { Dialog } from '../Molecules/Dialog';
import { FilePicker } from '../Molecules/FilePicker';
import { ProtectedTable } from '../Permissions/PermissionDenied';
import { collectionPreferences } from '../Preferences/collectionPreferences';
import { userPreferences } from '../Preferences/userPreferences';
import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin';
import { attachmentSettingsPromise, uploadFile } from './attachments';
import { AttachmentViewer } from './Viewer';
import { collectionPreferences } from '../Preferences/collectionPreferences';

export function AttachmentsPlugin(
props: Parameters<typeof ProtectedAttachmentsPlugin>[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { strictGetTable } from '../DataModel/tables';
import type { Attachment, Tables } from '../DataModel/types';
import { Dialog } from '../Molecules/Dialog';
import { hasPermission } from '../Permissions/helpers';
import { collectionPreferences } from '../Preferences/collectionPreferences';
import { ActionState } from './ActionState';
import type { AttachmentUploadSpec, EagerDataSet } from './Import';
import { PerformAttachmentTask } from './PerformAttachmentTask';
Expand All @@ -39,7 +40,6 @@ import {
saveForAttachmentUpload,
validateAttachmentFiles,
} from './utils';
import { collectionPreferences } from '../Preferences/collectionPreferences';

async function prepareForUpload(
dataSet: EagerDataSet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
fieldName: string &
(keyof SCHEMA['fields'] | keyof SCHEMA['toOneIndependent'])
): Promise<RA<BusinessRuleResult<SCHEMA>>> {
// REFACTOR: When checkField is called directly, the promises are not
// added to the public pendingPromise
/*
* REFACTOR: When checkField is called directly, the promises are not
* added to the public pendingPromise
*/

const field = this.resource.specifyTable.getField(fieldName);
if (field === undefined) return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ describe('getDateParser', () => {
new Date()
)
).toMatchInlineSnapshot(`
{
"formatters": [
[Function],
[Function],
],
"max": "9999-12-31",
"minLength": 10,
"parser": [Function],
"required": false,
"title": "Required Format: MM/DD/YYYY.",
"type": "date",
"validators": [
[Function],
],
"value": "2022-08-31",
"whiteSpaceSensitive": false,
}
`));
{
"formatters": [
[Function],
[Function],
],
"max": "9999-12-31",
"minLength": 10,
"parser": [Function],
"required": false,
"title": "Required Format: MM/DD/YYYY.",
"type": "date",
"validators": [
[Function],
],
"value": "2022-08-31",
"whiteSpaceSensitive": false,
}
`));

test('month-year', () =>
expect(getDateParser(undefined, 'month-year', undefined))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { GetSet, WritableArray } from '../../utils/types';
import { Link } from '../Atoms/Link';
import { pathIsOverlay } from '../Router/UnloadProtect';
import { scrollIntoView } from '../TreeView/helpers';
import { PreferenceType, usePrefDefinitions } from './index';
import type { PreferenceType } from './index';
import { usePrefDefinitions } from './index';

export function PreferencesAside({
activeCategory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LocalizedString } from 'typesafe-i18n';
import type { LocalizedString } from 'typesafe-i18n';

import { attachmentsText } from '../../localization/attachments';
import { preferencesText } from '../../localization/preferences';
import { queryText } from '../../localization/query';
Expand All @@ -8,13 +9,13 @@ import { treeText } from '../../localization/tree';
import { f } from '../../utils/functools';
import type { RA } from '../../utils/types';
import { ensure } from '../../utils/types';
import { camelToHuman } from '../../utils/utils';
import { genericTables } from '../DataModel/tables';
import { Tables } from '../DataModel/types';
import type { Tables } from '../DataModel/types';
import type { QueryView } from '../QueryBuilder/Header';
import type { StatLayout } from '../Statistics/types';
import type { GenericPreferences } from './types';
import { definePref } from './types';
import { camelToHuman } from '../../utils/utils';
import { QueryView } from '../QueryBuilder/Header';

const tableLabel = (tableName: keyof Tables): LocalizedString =>
genericTables[tableName]?.label ?? camelToHuman(tableName);
Expand Down
Loading