From 45d3fe0583389cc3f3706303860890f06e414303 Mon Sep 17 00:00:00 2001 From: Jason Melton <64045831+melton-jason@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:16:21 +0000 Subject: [PATCH 01/16] Lint code with ESLint and Prettier Triggered by f1aa17c41cc918f2e5365aa79922f9f101616922 on branch refs/heads/issue-5418 --- .../Preferences/UserDefinitions.tsx | 41 ++++++++----------- .../js_src/lib/components/TreeView/Row.tsx | 14 ++++--- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx index d925e9f3c7b..f3856c2fee4 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx @@ -1103,8 +1103,7 @@ export const userPreferenceDefinitions = { }, }, recordSet: { - title: () => - tableLabel('RecordSet'), + title: () => tableLabel('RecordSet'), items: { recordToOpen: definePref<'first' | 'last'>({ title: preferencesText.recordSetRecordToOpen(), @@ -1348,7 +1347,9 @@ export const userPreferenceDefinitions = { defaultValue: false, type: 'java.lang.Boolean', }), - orderByField: definePref<'fullName' | 'name' | 'nodeNumber' | 'rankId'>({ + orderByField: definePref< + 'fullName' | 'name' | 'nodeNumber' | 'rankId' + >({ title: preferencesText.sortByField(), requiresReload: false, visible: true, @@ -1413,8 +1414,7 @@ export const userPreferenceDefinitions = { * This would be replaced with labels from schema once * schema is loaded */ - title: () => - tableLabel('Geography'), + title: () => tableLabel('Geography'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -1435,8 +1435,7 @@ export const userPreferenceDefinitions = { }, }, taxon: { - title: () => - tableLabel('Taxon'), + title: () => tableLabel('Taxon'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -1457,8 +1456,7 @@ export const userPreferenceDefinitions = { }, }, storage: { - title: () => - tableLabel('Storage'), + title: () => tableLabel('Storage'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -1479,8 +1477,7 @@ export const userPreferenceDefinitions = { }, }, geologicTimePeriod: { - title: () => - tableLabel('GeologicTimePeriod'), + title: () => tableLabel('GeologicTimePeriod'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -1501,8 +1498,7 @@ export const userPreferenceDefinitions = { }, }, lithoStrat: { - title: () => - tableLabel('LithoStrat'), + title: () => tableLabel('LithoStrat'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -1523,8 +1519,7 @@ export const userPreferenceDefinitions = { }, }, tectonicUnit: { - title: () => - tableLabel('TectonicUnit'), + title: () => tableLabel('TectonicUnit'), items: { treeAccentColor: definePref({ title: preferencesText.treeAccentColor(), @@ -2052,10 +2047,7 @@ import('../DataModel/tables') ), 'Unable to find tree full name value' ); - overwriteReadOnly( - name, - 'title', - getField(tables.Taxon, 'name').label); + overwriteReadOnly(name, 'title', getField(tables.Taxon, 'name').label); overwriteReadOnly( fullName, 'title', @@ -2067,7 +2059,8 @@ import('../DataModel/tables') // Update titles for orderByField const treeOrderByBehavior = - userPreferenceDefinitions.treeEditor.subCategories.behavior.items.orderByField; + userPreferenceDefinitions.treeEditor.subCategories.behavior.items + .orderByField; if ('values' in treeOrderByBehavior) { const orderByValues = treeOrderByBehavior.values as RA<{ readonly value: string; @@ -2112,7 +2105,7 @@ import('../DataModel/tables') overwriteReadOnly( rankId, 'title', - getField(tables.Taxon,'rankId').label + getField(tables.Taxon, 'rankId').label ); overwriteReadOnly( nodeNumber, @@ -2120,11 +2113,13 @@ import('../DataModel/tables') getField(tables.Taxon, 'nodeNumber').label ); } else { - softError('Unable to replace the tree preferences item title for orderByField'); + softError( + 'Unable to replace the tree preferences item title for orderByField' + ); } }) ) - + // Not using softFail here to avoid circular dependency .catch(console.error); diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx index 5e33a25ea64..0d6c06b08c8 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Row.tsx @@ -87,11 +87,15 @@ export function TreeRow({ void getRows(row.nodeId).then((fetchedRows: RA) => { const sortedRows = Array.from(fetchedRows).sort( sortFunction( - orderByField === 'rankId' ? (row) => row.rankId : - orderByField === 'nodeNumber' ? (row) => row.nodeNumber : - orderByField === 'name' ? (row) => row.name : - orderByField === 'fullName' ? (row) => row.fullName : - () => 0 + orderByField === 'rankId' + ? (row) => row.rankId + : orderByField === 'nodeNumber' + ? (row) => row.nodeNumber + : orderByField === 'name' + ? (row) => row.name + : orderByField === 'fullName' + ? (row) => row.fullName + : () => 0 ) ); destructorCalled ? undefined : setRows(sortedRows); From 9b719672a4832ed6c370aebe3fb200ca7be6b0b0 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 14 Jan 2025 02:37:51 -0600 Subject: [PATCH 02/16] Alter required status for Geo fields in WB --- .../js_src/lib/components/DataModel/schemaOverrides.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts index 971f4f33524..06d45420603 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaOverrides.ts @@ -237,6 +237,13 @@ const fieldOverwrites: typeof globalFieldOverrides = { CollectionObject: { collectionObjectType: { visibility: 'optional' }, }, + CollectionObjectGroupType: { + type: { visibility: 'optional' }, + }, + CollectionObjectGroupJoin: { + precedence: { visibility: 'optional' }, + isSubstrate: { visibility: 'optional' }, + }, LoanPreparation: { isResolved: { visibility: 'optional' }, }, From 634b9986ae27048b1fdd2f3b7c1755793eb1f72e Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 14 Jan 2025 02:40:17 -0600 Subject: [PATCH 03/16] Add cojo businessrule to automatically set precedence --- specifyweb/businessrules/rules/cojo_rules.py | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index 3156c6d3ce7..556e6cca2b0 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -1,18 +1,17 @@ -import os -import sys from enum import Enum +from django.db.models import Max + from specifyweb.businessrules.exceptions import BusinessRuleException from specifyweb.businessrules.orm_signal_handler import orm_signal_handler from specifyweb.specify.models import Collectionobjectgroupjoin + class COGType(Enum): DISCRETE = "Discrete" CONSOLIDATED = "Consolidated" DRILL_CORE = "Drill Core" -def is_running_tests(): - return any(module in sys.modules for module in ('pytest', 'unittest')) @orm_signal_handler('pre_save', 'Collectionobjectgroupjoin') def cojo_pre_save(cojo): @@ -41,24 +40,33 @@ def cojo_pre_save(cojo): cojo.childcog is not None and cojo.childcog.cojo is not None and cojo.childcog.cojo.id is not cojo.id - and not is_running_tests() ): - raise BusinessRuleException('ChildCog is already in use as a child in another COG.') + raise BusinessRuleException( + 'ChildCog is already in use as a child in another COG.') if ( cojo.childco is not None and cojo.childco.cojo is not None and cojo.childco.cojo.id is not cojo.id - and not is_running_tests() ): - raise BusinessRuleException('ChildCo is already in use as a child in another COG.') - + raise BusinessRuleException( + 'ChildCo is already in use as a child in another COG.') + + if cojo.precedence is None: + others = Collectionobjectgroupjoin.objects.filter( + parentcog=cojo.parentcog + ) + top = others.aggregate(Max('precedence'))['precedence__max'] + cojo.precedence = 0 if top is None else top + 1 + + @orm_signal_handler('post_save', 'Collectionobjectgroupjoin') def cojo_post_save(cojo): """ For Consolidated COGs, mark the first CO child as primary if none have been set by the user """ - co_children = Collectionobjectgroupjoin.objects.filter(parentcog=cojo.parentcog, childco__isnull=False) + co_children = Collectionobjectgroupjoin.objects.filter( + parentcog=cojo.parentcog, childco__isnull=False) if len(co_children) > 0 and not co_children.filter(isprimary=True).exists() and cojo.parentcog.cogtype.type == COGType.CONSOLIDATED.value: first_child = co_children.first() first_child.isprimary = True From 7139a76489056978c3d093a3d93ace7500ac3d06 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 14 Jan 2025 02:42:48 -0600 Subject: [PATCH 04/16] Support one-to-one relationships in the WB --- .../workbench/upload/upload_plan_schema.py | 7 ++- specifyweb/workbench/upload/upload_table.py | 44 +++++++++++++++---- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py index e148307fa46..dc7b1c6873b 100644 --- a/specifyweb/workbench/upload/upload_plan_schema.py +++ b/specifyweb/workbench/upload/upload_plan_schema.py @@ -1,13 +1,12 @@ from functools import reduce -from os import name -from typing import Dict, Any, Optional, Union, Tuple +from typing import Dict, Union, Tuple import logging -from specifyweb.specify.datamodel import datamodel, Table, Relationship +from specifyweb.specify.datamodel import datamodel, Table from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable from .tomany import ToManyRecord -from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord +from .treerecord import TreeRank, TreeRecord, MustMatchTreeRecord from .uploadable import Uploadable from .column_options import ColumnOptions from .scoping import DEFERRED_SCOPING diff --git a/specifyweb/workbench/upload/upload_table.py b/specifyweb/workbench/upload/upload_table.py index e361ebb478f..dd036708d24 100644 --- a/specifyweb/workbench/upload/upload_table.py +++ b/specifyweb/workbench/upload/upload_table.py @@ -367,7 +367,9 @@ def _handle_row(self, force_upload: bool) -> UploadResult: info = ReportInfo(tableName=self.name, columns=[pr.column for pr in self.parsedFields], treeInfo=None) - toOneResults_ = self._process_to_ones() + local_to_ones, remote_to_ones = separate_to_ones(model, self.toOne) + + toOneResults_ = self._process_to_ones(local_to_ones) multi_one_to_one = lambda field, result: self.toOne[field].is_one_to_one() and isinstance(result.record_result, MatchedMultiple) @@ -412,13 +414,13 @@ def _handle_row(self, force_upload: bool) -> UploadResult: if match: return UploadResult(match, toOneResults, {}) - return self._do_upload(model, toOneResults, info) + return self._do_upload(model, toOneResults, remote_to_ones, info) - def _process_to_ones(self) -> Dict[str, UploadResult]: + def _process_to_ones(self, toOnes: Dict[str, BoundUploadable]) -> Dict[str, UploadResult]: return { fieldname: to_one_def.process_row() for fieldname, to_one_def in - sorted(self.toOne.items(), key=lambda kv: kv[0]) # make the upload order deterministic + sorted(toOnes.items(), key=lambda kv: kv[0]) # make the upload order deterministic } def _match(self, model, toOneResults: Dict[str, UploadResult], toManyFilters: FilterPack, info: ReportInfo) -> Union[Matched, MatchedMultiple, None]: @@ -463,7 +465,7 @@ def _match(self, model, toOneResults: Dict[str, UploadResult], toManyFilters: Fi else: return None - def _do_upload(self, model, toOneResults: Dict[str, UploadResult], info: ReportInfo) -> UploadResult: + def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: Dict[str, BoundUploadable], info: ReportInfo) -> UploadResult: missing_requireds = [ # TODO: there should probably be a different structure for # missing required fields than ParseFailure @@ -516,12 +518,14 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], info: ReportI self.auditor.insert(uploaded, self.uploadingAgentId, None) + remoteToOneResults = _upload_remote_to_ones(model, uploaded.id, remoteToOnes) + toManyResults = { fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, records) for fieldname, records in sorted(self.toMany.items(), key=lambda kv: kv[0]) # make the upload order deterministic } - return UploadResult(Uploaded(uploaded.id, info, picklist_additions), toOneResults, toManyResults) + return UploadResult(Uploaded(uploaded.id, info, picklist_additions), {**toOneResults, **remoteToOneResults}, toManyResults) def _do_insert(self, model, **attrs) -> Any: return model.objects.create(**attrs) @@ -547,13 +551,13 @@ def must_match(self) -> bool: def force_upload_row(self) -> UploadResult: raise Exception('trying to force upload of must-match table') - def _process_to_ones(self) -> Dict[str, UploadResult]: + def _process_to_ones(self, toOnes: Dict[str, BoundUploadable]) -> Dict[str, UploadResult]: return { fieldname: to_one_def.match_row() - for fieldname, to_one_def in self.toOne.items() + for fieldname, to_one_def in toOnes.items() } - def _do_upload(self, model, toOneResults: Dict[str, UploadResult], info: ReportInfo) -> UploadResult: + def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: Dict[str, BoundUploadable], info: ReportInfo) -> UploadResult: return UploadResult(NoMatch(info), toOneResults, {}) @@ -588,3 +592,25 @@ def _upload_to_manys(parent_model, parent_id, parent_field, uploadingAgentId: Op ).force_upload_row() for record in records ] + +def _upload_remote_to_ones(parent_model, parent_id, remoteToOnes: Dict[str, BoundUploadTable]) -> Dict[str, UploadResult]: + toOnes: Dict[str, UploadResult] = dict() + for field_name, upload_table in sorted(remoteToOnes.items(), key=lambda kv: kv[0]): + related_field = parent_model._meta.get_field(field_name).field + related_column = related_field.attname + static = {**upload_table.static, related_column: parent_id} + toOnes[field_name] = upload_table._replace(static=static).process_row() + return toOnes + + +def separate_to_ones(parent_model, toOnes: Dict[str, BoundUploadable]): + remote_to_ones = dict() + local_to_ones = dict() + + for field_name, uploadable in toOnes.items(): + field = parent_model._meta.get_field(field_name) + if field.concrete: + local_to_ones[field_name] = uploadable + else: + remote_to_ones[field_name] = uploadable + return local_to_ones, remote_to_ones From 04b576e206d14a416d8edca9fbe4f74bebedd669 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 16 Jan 2025 00:15:10 -0600 Subject: [PATCH 05/16] Use BoundUploadTable for remoteToOne relationships --- specifyweb/workbench/upload/upload_table.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/specifyweb/workbench/upload/upload_table.py b/specifyweb/workbench/upload/upload_table.py index dd036708d24..cc7290f4ee8 100644 --- a/specifyweb/workbench/upload/upload_table.py +++ b/specifyweb/workbench/upload/upload_table.py @@ -1,7 +1,7 @@ import logging from functools import reduce -from typing import List, Dict, Any, NamedTuple, Union, Optional, Set, Callable, Literal, cast +from typing import List, Dict, Any, NamedTuple, Union, Optional, Set, Callable, Literal, Tuple, cast from django.db import transaction, IntegrityError @@ -518,7 +518,11 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: self.auditor.insert(uploaded, self.uploadingAgentId, None) - remoteToOneResults = _upload_remote_to_ones(model, uploaded.id, remoteToOnes) + remoteToOneResults = { + fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, [upload_table])[0] + for fieldname, upload_table in + sorted(remoteToOnes.items(), key=lambda kv: kv[0]) + } toManyResults = { fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, records) @@ -593,17 +597,7 @@ def _upload_to_manys(parent_model, parent_id, parent_field, uploadingAgentId: Op for record in records ] -def _upload_remote_to_ones(parent_model, parent_id, remoteToOnes: Dict[str, BoundUploadTable]) -> Dict[str, UploadResult]: - toOnes: Dict[str, UploadResult] = dict() - for field_name, upload_table in sorted(remoteToOnes.items(), key=lambda kv: kv[0]): - related_field = parent_model._meta.get_field(field_name).field - related_column = related_field.attname - static = {**upload_table.static, related_column: parent_id} - toOnes[field_name] = upload_table._replace(static=static).process_row() - return toOnes - - -def separate_to_ones(parent_model, toOnes: Dict[str, BoundUploadable]): +def separate_to_ones(parent_model, toOnes: Dict[str, BoundUploadable]) -> Tuple[Dict[str, BoundUploadable], Dict[str, BoundUploadable]]: remote_to_ones = dict() local_to_ones = dict() From 54c95f3deb9d3199fd3a01413c3af1ea6c4c4184 Mon Sep 17 00:00:00 2001 From: Jason Melton <64045831+melton-jason@users.noreply.github.com> Date: Thu, 16 Jan 2025 06:19:49 +0000 Subject: [PATCH 06/16] Lint code with ESLint and Prettier Triggered by 79a2adcc724392dc39bee7452d887fa3f7110964 on branch refs/heads/issue-5418 --- .../js_src/lib/components/InitialContext/systemInfo.ts | 4 ++-- .../js_src/lib/components/WbPlanView/navigator.ts | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index dbc7015261d..8ff7a789077 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -22,7 +22,7 @@ type SystemInfo = { readonly institution_guid: LocalizedString; readonly isa_number: LocalizedString; readonly stats_url: string | null; - readonly discipline_type: string + readonly discipline_type: string; }; let systemInfo: SystemInfo; @@ -45,7 +45,7 @@ export const fetchContext = load( collection: systemInfo.collection, collectionGUID: systemInfo.collection_guid, isaNumber: systemInfo.isa_number, - disciplineType: systemInfo.discipline_type + disciplineType: systemInfo.discipline_type, }, /* * I don't know if the receiving server handles GET parameters in a diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts index 769c5990df5..b7a1b46606c 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts @@ -542,13 +542,12 @@ export function getMappingLineData({ .filter((field) => { let isIncluded = true; - const disciplineType = getSystemInfo().discipline_type?.toLowerCase() - const geoPaleoDisciplines= ['geology', 'invertpaleo', 'vertpaleo'] + const disciplineType = + getSystemInfo().discipline_type?.toLowerCase(); + const geoPaleoDisciplines = ['geology', 'invertpaleo', 'vertpaleo']; if ( field.name === 'age' && - !geoPaleoDisciplines.includes( - disciplineType - ) + !geoPaleoDisciplines.includes(disciplineType) ) { return false; } From 4bf969183ab4614336ce83bbce7c2fe32f36a2a0 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 16 Jan 2025 00:26:04 -0600 Subject: [PATCH 07/16] Add comment --- specifyweb/workbench/upload/upload_table.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/specifyweb/workbench/upload/upload_table.py b/specifyweb/workbench/upload/upload_table.py index cc7290f4ee8..b46cc8f058c 100644 --- a/specifyweb/workbench/upload/upload_table.py +++ b/specifyweb/workbench/upload/upload_table.py @@ -518,6 +518,9 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: self.auditor.insert(uploaded, self.uploadingAgentId, None) + # Like to-many relationships, remote to-one relationships can not be + # directly inserted with the main base record, and instead are + # uploaded with a reference to the base record remoteToOneResults = { fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, [upload_table])[0] for fieldname, upload_table in From e00513f3f116c58bca6c582a8fd8af8b7fed31dc Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 16 Jan 2025 09:35:36 -0600 Subject: [PATCH 08/16] Upload remoteToOne relationships as toMany --- specifyweb/workbench/upload/upload_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/workbench/upload/upload_table.py b/specifyweb/workbench/upload/upload_table.py index b46cc8f058c..a8fcd18ae84 100644 --- a/specifyweb/workbench/upload/upload_table.py +++ b/specifyweb/workbench/upload/upload_table.py @@ -522,7 +522,7 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: # directly inserted with the main base record, and instead are # uploaded with a reference to the base record remoteToOneResults = { - fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, [upload_table])[0] + fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, [upload_table]) for fieldname, upload_table in sorted(remoteToOnes.items(), key=lambda kv: kv[0]) } @@ -532,7 +532,7 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: for fieldname, records in sorted(self.toMany.items(), key=lambda kv: kv[0]) # make the upload order deterministic } - return UploadResult(Uploaded(uploaded.id, info, picklist_additions), {**toOneResults, **remoteToOneResults}, toManyResults) + return UploadResult(Uploaded(uploaded.id, info, picklist_additions), toOneResults, {**remoteToOneResults, **toManyResults}) def _do_insert(self, model, **attrs) -> Any: return model.objects.create(**attrs) From a8797d38538060277c6ea952f9313b64730016f9 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Fri, 17 Jan 2025 14:44:36 +0000 Subject: [PATCH 09/16] Lint code with ESLint and Prettier Triggered by 1d14140b792bde093cfff63ca65931d7ce5f0316 on branch refs/heads/issue-5418 --- .../lib/components/QueryComboBox/index.tsx | 60 ++++++++++--------- .../lib/components/SearchDialog/index.tsx | 12 +++- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx index b34927c8c7c..1e776d86977 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx @@ -524,41 +524,45 @@ export function QueryComboBox({ .map(serializeResource) .map(({ fieldName, startValue }) => fieldName === 'rankId' - ? { - field: 'rankId', - isRelationship: false, - isNot: false, - operation: 'less', - value: startValue, - } - : fieldName === 'nodeNumber' - ? { - field: 'nodeNumber', - isRelationship: false, - operation: 'between', - isNot: true, - value: startValue, - } - : fieldName === 'collectionRelTypeId' ? { - field: 'id', + field: 'rankId', isRelationship: false, - operation: 'in', isNot: false, + operation: 'less', value: startValue, } - : fieldName === 'taxonTreeDefId' + : fieldName === 'nodeNumber' ? { - field: 'definition', - isRelationship: true, - operation: 'in', - isNot: false, - value: startValue + field: 'nodeNumber', + isRelationship: false, + operation: 'between', + isNot: true, + value: startValue, } - : f.error(`extended filter not created`, { - fieldName, - startValue, - })) + : fieldName === 'collectionRelTypeId' + ? { + field: 'id', + isRelationship: false, + operation: 'in', + isNot: false, + value: startValue, + } + : fieldName === 'taxonTreeDefId' + ? { + field: 'definition', + isRelationship: true, + operation: 'in', + isNot: false, + value: startValue, + } + : f.error( + `extended filter not created`, + { + fieldName, + startValue, + } + ) + ) ), }) : undefined diff --git a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx index 9b5bd1d3213..ea01ebff46b 100644 --- a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx @@ -139,7 +139,13 @@ const filterResults = ( function testFilter( resource: SpecifyResource, - { operation, field, value, isNot, isRelationship }: QueryComboBoxFilter + { + operation, + field, + value, + isNot, + isRelationship, + }: QueryComboBoxFilter ): boolean { const values = value.split(',').map(f.trim); const result = @@ -151,7 +157,9 @@ function testFilter( values.some((value) => { const fieldValue = resource.get(field); // eslint-disable-next-line eqeqeq - return isRelationship ? value == strictIdFromUrl(fieldValue!).toString() : value == fieldValue + return isRelationship + ? value == strictIdFromUrl(fieldValue!).toString() + : value == fieldValue; }) : operation === 'less' ? values.every((value) => (resource.get(field) ?? 0) < value) From b1f73526dc13bedbc2329d646661d21247743340 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 20 Jan 2025 21:48:25 -0600 Subject: [PATCH 10/16] Revert remoteToOne changes in backend --- .../workbench/upload/upload_plan_schema.py | 7 +-- specifyweb/workbench/upload/upload_table.py | 43 +++++-------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py index dc7b1c6873b..e148307fa46 100644 --- a/specifyweb/workbench/upload/upload_plan_schema.py +++ b/specifyweb/workbench/upload/upload_plan_schema.py @@ -1,12 +1,13 @@ from functools import reduce -from typing import Dict, Union, Tuple +from os import name +from typing import Dict, Any, Optional, Union, Tuple import logging -from specifyweb.specify.datamodel import datamodel, Table +from specifyweb.specify.datamodel import datamodel, Table, Relationship from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable from .tomany import ToManyRecord -from .treerecord import TreeRank, TreeRecord, MustMatchTreeRecord +from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord from .uploadable import Uploadable from .column_options import ColumnOptions from .scoping import DEFERRED_SCOPING diff --git a/specifyweb/workbench/upload/upload_table.py b/specifyweb/workbench/upload/upload_table.py index a8fcd18ae84..e361ebb478f 100644 --- a/specifyweb/workbench/upload/upload_table.py +++ b/specifyweb/workbench/upload/upload_table.py @@ -1,7 +1,7 @@ import logging from functools import reduce -from typing import List, Dict, Any, NamedTuple, Union, Optional, Set, Callable, Literal, Tuple, cast +from typing import List, Dict, Any, NamedTuple, Union, Optional, Set, Callable, Literal, cast from django.db import transaction, IntegrityError @@ -367,9 +367,7 @@ def _handle_row(self, force_upload: bool) -> UploadResult: info = ReportInfo(tableName=self.name, columns=[pr.column for pr in self.parsedFields], treeInfo=None) - local_to_ones, remote_to_ones = separate_to_ones(model, self.toOne) - - toOneResults_ = self._process_to_ones(local_to_ones) + toOneResults_ = self._process_to_ones() multi_one_to_one = lambda field, result: self.toOne[field].is_one_to_one() and isinstance(result.record_result, MatchedMultiple) @@ -414,13 +412,13 @@ def _handle_row(self, force_upload: bool) -> UploadResult: if match: return UploadResult(match, toOneResults, {}) - return self._do_upload(model, toOneResults, remote_to_ones, info) + return self._do_upload(model, toOneResults, info) - def _process_to_ones(self, toOnes: Dict[str, BoundUploadable]) -> Dict[str, UploadResult]: + def _process_to_ones(self) -> Dict[str, UploadResult]: return { fieldname: to_one_def.process_row() for fieldname, to_one_def in - sorted(toOnes.items(), key=lambda kv: kv[0]) # make the upload order deterministic + sorted(self.toOne.items(), key=lambda kv: kv[0]) # make the upload order deterministic } def _match(self, model, toOneResults: Dict[str, UploadResult], toManyFilters: FilterPack, info: ReportInfo) -> Union[Matched, MatchedMultiple, None]: @@ -465,7 +463,7 @@ def _match(self, model, toOneResults: Dict[str, UploadResult], toManyFilters: Fi else: return None - def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: Dict[str, BoundUploadable], info: ReportInfo) -> UploadResult: + def _do_upload(self, model, toOneResults: Dict[str, UploadResult], info: ReportInfo) -> UploadResult: missing_requireds = [ # TODO: there should probably be a different structure for # missing required fields than ParseFailure @@ -518,21 +516,12 @@ def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: self.auditor.insert(uploaded, self.uploadingAgentId, None) - # Like to-many relationships, remote to-one relationships can not be - # directly inserted with the main base record, and instead are - # uploaded with a reference to the base record - remoteToOneResults = { - fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, [upload_table]) - for fieldname, upload_table in - sorted(remoteToOnes.items(), key=lambda kv: kv[0]) - } - toManyResults = { fieldname: _upload_to_manys(model, uploaded.id, fieldname, self.uploadingAgentId, self.auditor, self.cache, records) for fieldname, records in sorted(self.toMany.items(), key=lambda kv: kv[0]) # make the upload order deterministic } - return UploadResult(Uploaded(uploaded.id, info, picklist_additions), toOneResults, {**remoteToOneResults, **toManyResults}) + return UploadResult(Uploaded(uploaded.id, info, picklist_additions), toOneResults, toManyResults) def _do_insert(self, model, **attrs) -> Any: return model.objects.create(**attrs) @@ -558,13 +547,13 @@ def must_match(self) -> bool: def force_upload_row(self) -> UploadResult: raise Exception('trying to force upload of must-match table') - def _process_to_ones(self, toOnes: Dict[str, BoundUploadable]) -> Dict[str, UploadResult]: + def _process_to_ones(self) -> Dict[str, UploadResult]: return { fieldname: to_one_def.match_row() - for fieldname, to_one_def in toOnes.items() + for fieldname, to_one_def in self.toOne.items() } - def _do_upload(self, model, toOneResults: Dict[str, UploadResult], remoteToOnes: Dict[str, BoundUploadable], info: ReportInfo) -> UploadResult: + def _do_upload(self, model, toOneResults: Dict[str, UploadResult], info: ReportInfo) -> UploadResult: return UploadResult(NoMatch(info), toOneResults, {}) @@ -599,15 +588,3 @@ def _upload_to_manys(parent_model, parent_id, parent_field, uploadingAgentId: Op ).force_upload_row() for record in records ] - -def separate_to_ones(parent_model, toOnes: Dict[str, BoundUploadable]) -> Tuple[Dict[str, BoundUploadable], Dict[str, BoundUploadable]]: - remote_to_ones = dict() - local_to_ones = dict() - - for field_name, uploadable in toOnes.items(): - field = parent_model._meta.get_field(field_name) - if field.concrete: - local_to_ones[field_name] = uploadable - else: - remote_to_ones[field_name] = uploadable - return local_to_ones, remote_to_ones From 07dfcf2a227b45dc70d465e6d218929292d59812 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 20 Jan 2025 22:55:03 -0600 Subject: [PATCH 11/16] Parse the remote side of one-to-ones as toMany on frontend --- .../components/FormCells/PickListEditor.tsx | 3 +-- .../components/FormSliders/RecordSelector.tsx | 4 +--- .../RecordSelectorFromCollection.tsx | 3 +-- .../lib/components/WbPlanView/autoMapper.ts | 6 ++++- .../lib/components/WbPlanView/helpers.ts | 8 +++++-- .../components/WbPlanView/mappingHelpers.ts | 15 +++++++++++++ .../lib/components/WbPlanView/modelHelpers.ts | 7 ++++-- .../lib/components/WbPlanView/navigator.ts | 19 +++++++++++----- .../js_src/lib/hooks/useCollection.tsx | 22 ++++++++----------- 9 files changed, 57 insertions(+), 30 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx index 52b0400bc9f..34e569c7e8c 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx @@ -56,8 +56,7 @@ export function PickListEditor({ relationship={relationship} sortField={undefined} onAdd={ - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' + relationship.type.includes('-to-many') ? undefined : ([resource]): void => void resource.set(relationship.name, resource as never) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx index 5d18eb99be4..371da11c494 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx @@ -84,9 +84,7 @@ export function useRecordSelector({ ); const isToOne = - field === undefined - ? false - : !relationshipIsToMany(field) || field.type === 'zero-to-one'; + field === undefined ? false : !field.type.includes('-to-many'); const handleResourcesSelected = React.useMemo( () => diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index aa2c572b570..ab40df892e8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -58,8 +58,7 @@ export function RecordSelectorFromCollection({ const isDependent = collection instanceof DependentCollection; const isLazy = collection instanceof LazyCollection; - const isToOne = - !relationshipIsToMany(relationship) || relationship.type === 'zero-to-one'; + const isToOne = !relationship.type.includes('-to-many'); // Listen for changes to collection React.useEffect( diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/autoMapper.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/autoMapper.ts index 1192e618a8f..f78001c9edc 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/autoMapper.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/autoMapper.ts @@ -30,6 +30,7 @@ import { getNameFromTreeRankName, getNumberFromToManyIndex, mappingPathToString, + relationshipIsRemoteToOne, relationshipIsToMany, valueIsToManyIndex, valueIsTreeRank, @@ -809,7 +810,10 @@ export class AutoMapper { .forEach((relationship) => { const localPath = [...mappingPath, relationship.name]; - if (relationshipIsToMany(relationship)) + if ( + relationshipIsToMany(relationship) || + relationshipIsRemoteToOne(relationship) + ) localPath.push(formatToManyIndex(1)); const newDepthLevel = localPath.length; diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts index 7b7dd213d92..a540ff24c2b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts @@ -28,6 +28,7 @@ import { formatToManyIndex, formatTreeRank, mappingPathToString, + relationshipIsRemoteToOne, relationshipIsToMany, valueIsToManyIndex, valueIsTreeRank, @@ -266,10 +267,13 @@ export function mutateMappingPath({ const table = getTable(parentTableName ?? ''); const currentField = table?.getField(mappingPath[index] ?? ''); const isCurrentToMany = - currentField?.isRelationship === true && relationshipIsToMany(currentField); + currentField?.isRelationship === true && + (relationshipIsToMany(currentField) || + relationshipIsRemoteToOne(currentField)); const newField = table?.getField(newValue); const isNewToMany = - newField?.isRelationship === true && relationshipIsToMany(newField); + newField?.isRelationship === true && + (relationshipIsToMany(newField) || relationshipIsRemoteToOne(newField)); const isNewTree = newField?.isRelationship === true && isTreeTable(newField.relatedTable.name); diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts index dc65961182d..6265f184cdc 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts @@ -22,6 +22,21 @@ export const relationshipIsToMany = ( relationship?.type.includes('-to-many') === true || relationship?.type === 'zero-to-one'; +/** + * Returns whether the relatation is one-to-one from the remote side + * (the foreign key exists on the other table of the relationship) + * + * In the WorkBench, remote one-to-one relationships are parsed as to-many + * in the upload plan + * + * See https://github.com/specify/specify7/pull/6073#discussion_r1915397675 + */ +export const relationshipIsRemoteToOne = ( + relationship: Relationship | undefined +): boolean => + relationship?.type === 'one-to-one' && + relationship.databaseColumn === undefined; + export type FieldType = Exclude; /** Returns whether a value is a -to-many index (e.x #1, #2, etc...) */ diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/modelHelpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/modelHelpers.ts index 9acc365b8c9..36a4ca97c62 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/modelHelpers.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/modelHelpers.ts @@ -18,6 +18,7 @@ import type { MappingPath } from './Mapper'; import { formatTreeRank, getNumberFromToManyIndex, + relationshipIsRemoteToOne, relationshipIsToMany, valueIsToManyIndex, valueIsTreeRank, @@ -103,8 +104,10 @@ export function findRequiredMissingFields( // Disable circular relationships (isCircularRelationship(parentRelationship, relationship) || // Skip -to-many inside -to-many - (relationshipIsToMany(parentRelationship) && - relationshipIsToMany(relationship))) + ((relationshipIsToMany(parentRelationship) || + relationshipIsRemoteToOne(parentRelationship)) && + (relationshipIsToMany(relationship) || + relationshipIsRemoteToOne(relationship)))) ) return []; diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts index b7a1b46606c..1dd5a3291a1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts @@ -36,6 +36,7 @@ import { getNameFromTreeDefinitionName, getNameFromTreeRankName, parsePartialField, + relationshipIsRemoteToOne, relationshipIsToMany, valueIsPartialField, valueIsToManyIndex, @@ -121,7 +122,8 @@ function navigator({ if (next === undefined) return; const childrenAreToManyElements = - relationshipIsToMany(parentRelationship) && + (relationshipIsToMany(parentRelationship) || + relationshipIsRemoteToOne(parentRelationship)) && !valueIsToManyIndex(parentPartName) && !valueIsTreeMeta(parentPartName); @@ -328,7 +330,9 @@ export function getMappingLineData({ internalState.defaultValue, ]); - const isToOne = parentRelationship?.type === 'zero-to-one'; + const isToOne = + parentRelationship?.type === 'one-to-one' || + parentRelationship?.type === 'zero-to-one'; const toManyLimit = isToOne ? 1 : Number.POSITIVE_INFINITY; const additional = maxMappedElementNumber < toManyLimit @@ -588,8 +592,10 @@ export function getMappingLineData({ parentRelationship === undefined || (!isCircularRelationship(parentRelationship, field) && !( - relationshipIsToMany(field) && - relationshipIsToMany(parentRelationship) + (relationshipIsToMany(field) || + relationshipIsRemoteToOne(field)) && + (relationshipIsToMany(parentRelationship) || + relationshipIsRemoteToOne(parentRelationship)) )); isIncluded &&= @@ -611,7 +617,10 @@ export function getMappingLineData({ * Hide -to-many relationships to a tree table as they are * not supported by the WorkBench */ - !relationshipIsToMany(field) || + !( + relationshipIsToMany(field) || + relationshipIsRemoteToOne(field) + ) || !isTreeTable(field.relatedTable.name); } diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 2ac9bff2452..be9d7b3c7a0 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -6,7 +6,6 @@ import type { SpecifyResource } from '../components/DataModel/legacyTypes'; import type { Relationship } from '../components/DataModel/specifyField'; import type { Collection } from '../components/DataModel/specifyTable'; import type { SubViewSortField } from '../components/FormParse/cells'; -import { relationshipIsToMany } from '../components/WbPlanView/mappingHelpers'; import type { GetOrSet } from '../utils/types'; import { overwriteReadOnly } from '../utils/types'; import { sortFunction } from '../utils/utils'; @@ -34,8 +33,7 @@ export function useCollection({ >( React.useCallback( async () => - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' + relationship.type.includes('-to-many') ? fetchToManyCollection({ parentResource, relationship, @@ -61,16 +59,14 @@ export function useCollection({ versionRef.current += 1; const localVersionRef = versionRef.current; - const fetchCollection = - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ? fetchToManyCollection({ - parentResource, - relationship, - sortBy, - filters, - }) - : fetchToOneCollection({ parentResource, relationship, filters }); + const fetchCollection = relationship.type.includes('-to-many') + ? fetchToManyCollection({ + parentResource, + relationship, + sortBy, + filters, + }) + : fetchToOneCollection({ parentResource, relationship, filters }); return fetchCollection.then((collection) => { if ( From 3a0adb6deab7c279fd3f0ba68594fd378fed7568 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 20 Jan 2025 23:00:49 -0600 Subject: [PATCH 12/16] Remove unused imports --- .../frontend/js_src/lib/components/FormCells/PickListEditor.tsx | 1 - .../js_src/lib/components/FormSliders/RecordSelector.tsx | 1 - .../lib/components/FormSliders/RecordSelectorFromCollection.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx index 34e569c7e8c..05e26277ce6 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/PickListEditor.tsx @@ -10,7 +10,6 @@ import type { Collection } from '../DataModel/specifyTable'; import { getTable } from '../DataModel/tables'; import type { PickList } from '../DataModel/types'; import { IntegratedRecordSelector } from '../FormSliders/IntegratedRecordSelector'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; export function PickListEditor({ resource, diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx index 371da11c494..e013ec223a0 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx @@ -8,7 +8,6 @@ import type { SpecifyResource } from '../DataModel/legacyTypes'; import type { Relationship } from '../DataModel/specifyField'; import type { SpecifyTable } from '../DataModel/specifyTable'; import { useSearchDialog } from '../SearchDialog'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Slider } from './Slider'; export type RecordSelectorProps = { diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index ab40df892e8..949228d9d95 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -14,7 +14,6 @@ import type { SpecifyResource } from '../DataModel/legacyTypes'; import { resourceOn } from '../DataModel/resource'; import type { Relationship } from '../DataModel/specifyField'; import type { Collection } from '../DataModel/specifyTable'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { RecordSelectorProps, RecordSelectorState, From eeadce6dfb16bdaaba063409e62f6d6fe96c2d33 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 20 Jan 2025 23:35:11 -0600 Subject: [PATCH 13/16] Map built-in celery states to WB statuses Fixes #1835 --- specifyweb/workbench/views.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py index a1f94d3e2d0..03201d6459a 100644 --- a/specifyweb/workbench/views.py +++ b/specifyweb/workbench/views.py @@ -1,6 +1,6 @@ import json import logging -from typing import List, Optional +from typing import List, Optional, Dict, Literal, get_args as get_typing_args from uuid import uuid4 from django import http @@ -12,8 +12,8 @@ from jsonschema.exceptions import ValidationError # type: ignore from specifyweb.middleware.general import require_GET, require_http_methods -from specifyweb.specify.api import create_obj, get_object_or_404, obj_to_data, \ - toJson, uri_for_model +from specifyweb.celery_tasks import CELERY_TASK_STATE +from specifyweb.specify.api import get_object_or_404 from specifyweb.specify.views import login_maybe_required, openapi from specifyweb.specify.models import Recordset, Specifyuser from specifyweb.notifications.models import Message @@ -35,6 +35,9 @@ class DataSetPT(PermissionTarget): transfer = PermissionTargetAction() create_recordset = PermissionTargetAction() +WorkbenchUpdateStatus = Literal["PROGRESS", "PENDING", "FAILURE"] + + def regularize_rows(ncols: int, rows: List[List]) -> List[List[str]]: n = ncols + 1 # extra row info such as disambiguation in hidden col at end @@ -93,11 +96,7 @@ def regularize(row: List) -> Optional[List]: }, "taskstatus": { "type": "string", - "enum": [ - "PROGRESS", - "PENDING", - "FAILURE", - ] + "enum": list(get_typing_args(WorkbenchUpdateStatus)) }, "uploaderstatus": { "type": "object", @@ -729,16 +728,26 @@ def status(request, ds_id: int) -> http.HttpResponse: if ds.uploaderstatus is None: return http.JsonResponse(None, safe=False) + + task_status_map: Dict[str, WorkbenchUpdateStatus] = { + CELERY_TASK_STATE.RECEIVED: "PENDING", + CELERY_TASK_STATE.STARTED: "PENDING", + CELERY_TASK_STATE.SUCCESS: "PENDING", + CELERY_TASK_STATE.RETRY: "FAILURE", + CELERY_TASK_STATE.REVOKED: "FAILURE", + } task = { 'uploading': tasks.upload, 'validating': tasks.upload, 'unuploading': tasks.unupload, }[ds.uploaderstatus['operation']] + result = task.AsyncResult(ds.uploaderstatus['taskid']) + status = { 'uploaderstatus': ds.uploaderstatus, - 'taskstatus': result.state, + 'taskstatus': task_status_map.get(result.state, result.state), 'taskinfo': result.info if isinstance(result.info, dict) else repr(result.info) } return http.JsonResponse(status) From 7be63160e3cd2f2e4256756324c8facd31488c52 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 21 Jan 2025 00:29:20 -0600 Subject: [PATCH 14/16] Renable cojo.childco and cojo.childcog parity businessrules --- specifyweb/businessrules/rules/cojo_rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index 556e6cca2b0..cd66eb53ffb 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -16,12 +16,12 @@ class COGType(Enum): @orm_signal_handler('pre_save', 'Collectionobjectgroupjoin') def cojo_pre_save(cojo): # Ensure the both the childcog and childco fields are not null. - # if cojo.childcog == None and cojo.childco == None: - # raise BusinessRuleException('Both childcog and childco cannot be null.') + if cojo.childcog == None and cojo.childco == None: + raise BusinessRuleException('Both childcog and childco cannot be null.') # Ensure the childcog and childco fields are not both set. - # if cojo.childcog != None and cojo.childco != None: - # raise BusinessRuleException('Both childcog and childco cannot be set.') + if cojo.childcog != None and cojo.childco != None: + raise BusinessRuleException('Both childcog and childco cannot be set.') # For records with the same parentcog field, there can be only one isPrimare field set to True. # So when a record is saved with isPrimary set to True, we need to set all other records with the same parentcog From d30f414d30d51591c147484f5a6c7322d3e30393 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 21 Jan 2025 00:31:11 -0600 Subject: [PATCH 15/16] Use identity comparator over equality comparator --- specifyweb/businessrules/rules/cojo_rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index cd66eb53ffb..8c01bfdacf6 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -16,11 +16,11 @@ class COGType(Enum): @orm_signal_handler('pre_save', 'Collectionobjectgroupjoin') def cojo_pre_save(cojo): # Ensure the both the childcog and childco fields are not null. - if cojo.childcog == None and cojo.childco == None: + if cojo.childcog is None and cojo.childco is None: raise BusinessRuleException('Both childcog and childco cannot be null.') # Ensure the childcog and childco fields are not both set. - if cojo.childcog != None and cojo.childco != None: + if cojo.childcog is not None and cojo.childco is not None: raise BusinessRuleException('Both childcog and childco cannot be set.') # For records with the same parentcog field, there can be only one isPrimare field set to True. From 3d8b3b67ffca378236c4f009a627634debbb343b Mon Sep 17 00:00:00 2001 From: Jason Melton <64045831+melton-jason@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:06:04 +0000 Subject: [PATCH 16/16] Lint code with ESLint and Prettier Triggered by 05eafa2de42b5bbc402387be4a194751d59a0069 on branch refs/heads/issue-5418 --- .../frontend/js_src/lib/components/Forms/SubView.tsx | 7 ++++--- .../frontend/js_src/lib/components/PickLists/fetch.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index a31b661de35..090378ed19e 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -139,10 +139,11 @@ export function SubView({ return ( - {!RECURSIVE_RENDERING_EXCEPTIONS.has(parentResource.specifyTable) && + {(!RECURSIVE_RENDERING_EXCEPTIONS.has(parentResource.specifyTable) && parentContext - .map(({ relationship }) => relationship) - .includes(relationship) || collection === false ? undefined : ( + .map(({ relationship }) => relationship) + .includes(relationship)) || + collection === false ? undefined : ( <> {isButton && ( rows .map((row) => row[fieldName] ?? '')