diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/Create.tsx b/specifyweb/frontend/js_src/lib/components/FormEditor/Create.tsx index af38578b3c6..67621064cbd 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/Create.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/Create.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useOutletContext } from 'react-router'; import { useNavigate } from 'react-router-dom'; import type { LocalizedString } from 'typesafe-i18n'; -import { useAsyncState } from '../../hooks/useAsyncState'; +import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; import { commonText } from '../../localization/common'; diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Line.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Line.tsx index 8964345844e..5a4538a4d8f 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Line.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Line.tsx @@ -29,7 +29,6 @@ import { formattedEntry, mappingPathToString, parsePartialField, - relationshipIsToMany, valueIsPartialField, } from '../WbPlanView/mappingHelpers'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; @@ -167,7 +166,7 @@ export function QueryLine({ canOpenMap = fieldName === 'latitude1' || fieldName === 'longitude1'; } else if (isMapped) fieldType = - dataModelField?.isRelationship && relationshipIsToMany(dataModelField) + isFormatted && mappingPath.at(-1) === `${schema.referenceSymbol}1` ? 'aggregator' : 'formatter'; diff --git a/specifyweb/stored_queries/format.py b/specifyweb/stored_queries/format.py index 5c0a0366107..ea0a8ce6718 100644 --- a/specifyweb/stored_queries/format.py +++ b/specifyweb/stored_queries/format.py @@ -49,12 +49,25 @@ def __init__(self, collection, user, replace_nulls): self.replace_nulls = replace_nulls self.aggregator_count = 0 - def getFormatterDef(self, specify_model: Table, formatter_name) -> Optional[ - Element]: + def getFormatterDef(self, specify_model: Table, formatter_name) -> Optional[Element]: def lookup(attr: str, val: str) -> Optional[Element]: return self.formattersDom.find( 'format[@%s=%s]' % (attr, quoteattr(val))) + def lookup_default(attr: str, val: str) -> Optional[Element]: + elements = self.formattersDom.findall('format[@%s=%s]' % (attr, quoteattr(val))) + for element in elements: + if element.get('default') == 'true': + return element + return None + + def lookup_name(name: str) -> Optional[Element]: + elements = self.formattersDom.findall('format[@name=%s]' % quoteattr(name)) + for element in elements: + if element.get('class') == specify_model.classname: + return element + return None + def getFormatterFromSchema() -> Element: try: formatter_name = Splocalecontainer.objects.get( @@ -65,20 +78,52 @@ def getFormatterFromSchema() -> Element: except Splocalecontainer.DoesNotExist: return None - return formatter_name and lookup('name', formatter_name) + if formatter_name: + return lookup_name(formatter_name) + else: + return None + + result = None + if formatter_name: + result = lookup_name(formatter_name) + + if result is not None: + return result - return (formatter_name and lookup('name', formatter_name)) \ - or getFormatterFromSchema() \ - or lookup('class', specify_model.classname) + result = lookup_default('class', specify_model.classname) + if result is not None: + return result - def getAggregatorDef(self, specify_model: Table, aggregator_name) -> \ - Optional[Element]: + result = getFormatterFromSchema() + if result is not None: + return result + + return lookup('class', specify_model.classname) + + def hasFormatterDef(self, specify_model: Table, formatter_name) -> bool: + if formatter_name is None: + return False + elements = self.formattersDom.findall('format[@name=%s]' % quoteattr(formatter_name)) + for element in elements: + if element.get('class') == specify_model.classname: + return True + return False + + def getAggregatorDef(self, specify_model: Table, aggregator_name) -> Optional[Element]: def lookup(attr: str, val: str) -> Optional[Element]: - return self.formattersDom.find( - 'aggregators/aggregator[@%s=%s]' % (attr, quoteattr(val))) + return self.formattersDom.find('aggregators/aggregator[@%s=%s]' % (attr, quoteattr(val))) + + def lookup_default(attr: str, val: str) -> Optional[Element]: + elements = self.formattersDom.findall('aggregators/aggregator[@%s=%s]' % (attr, quoteattr(val))) + for element in elements: + if element.get('default') == 'true': + return element + return None - return (aggregator_name and lookup('name', aggregator_name)) \ - or lookup('class', specify_model.classname) + result = None + if aggregator_name: + result = lookup('name', aggregator_name) + return result if result is not None else lookup_default('class', specify_model.classname) def catalog_number_is_numeric(self): return self.collection.catalognumformatname == 'CatalogNumberNumeric' @@ -201,13 +246,13 @@ def case_value_convert(value): return value == 'true' if switch_field_spec.get_f def aggregate(self, query: QueryConstruct, field: Union[Field, Relationship], rel_table: SQLTable, - aggregator_name, + aggregator_formatter_name, cycle_detector=[]) -> Label: logger.info('aggregating field %s on %s using %s', field, rel_table, - aggregator_name) + aggregator_formatter_name) specify_model = datamodel.get_table(field.relatedModelName, strict=True) - aggregatorNode = self.getAggregatorDef(specify_model, aggregator_name) + aggregatorNode = self.getAggregatorDef(specify_model, aggregator_formatter_name) cycle_with_self = [*cycle_detector, (field.relatedModelName, 'aggregating')] if ( cycle_detector is not None) else None if aggregatorNode is None: @@ -215,7 +260,9 @@ def aggregate(self, query: QueryConstruct, return literal(_text("")) logger.debug("using aggregator: %s", ElementTree.tostring(aggregatorNode)) - formatter_name = aggregatorNode.attrib.get('format', None) + formatter_name = aggregator_formatter_name + if not self.hasFormatterDef(specify_model, aggregator_formatter_name): + formatter_name = aggregatorNode.attrib.get('format', None) separator = aggregatorNode.attrib.get('separator', ',') order_by = aggregatorNode.attrib.get('orderfieldname', '') limit = aggregatorNode.attrib.get('count', '')