From 8b8ae1664ec204bbdf1b690158b8d4440591e37a Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Wed, 8 Mar 2023 14:44:09 -0600 Subject: [PATCH 001/132] Allow independent subviews Fixes #114 --- specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 5c3d7504ef4..e700d3f771a 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -231,7 +231,7 @@ export function SubView({ formType={formType} mode={ !isAttachmentMisconfigured && - relationship.isDependent() && + (relationship.isDependent() || isButton) && initialMode !== 'view' ? 'edit' : 'view' From a9a0affe07468aa53d484b18c6b03eade68138ef Mon Sep 17 00:00:00 2001 From: Max Patiiuk Date: Wed, 8 Mar 2023 15:49:24 -0600 Subject: [PATCH 002/132] Turn independent sub views into buttons Fixes #3127 --- .../frontend/js_src/lib/components/Forms/SubView.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index e700d3f771a..313942188ed 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -41,7 +41,7 @@ export function SubView({ mode: initialMode, parentFormType, formType: initialFormType, - isButton, + isButton: rawIsButton, viewName = relationship.relatedModel.view, icon = relationship.relatedModel.name, sortField: initialSortField, @@ -187,6 +187,9 @@ export function SubView({ [relationship, formType, sortField, setFormType, setSortField] ); + // See https://github.com/specify/specify7/issues/3127 + const isButton = + rawIsButton || (initialMode === 'edit' && !relationship.isDependent()); const [isOpen, _, handleClose, handleToggle] = useBooleanState(!isButton); const [isAttachmentConfigured] = usePromise(attachmentSettingsPromise, true); @@ -231,6 +234,11 @@ export function SubView({ formType={formType} mode={ !isAttachmentMisconfigured && + /* + * Only button subview's can display independent resources + * See https://github.com/specify/specify7/pull/3125#issue-1615911079 + * for reasons why. + */ (relationship.isDependent() || isButton) && initialMode !== 'view' ? 'edit' From 8db292583820d2948d0eb8f264d29d9e8851fc89 Mon Sep 17 00:00:00 2001 From: Jason Melton <64045831+melton-jason@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:09:17 +0000 Subject: [PATCH 003/132] Lint code with ESLint and Prettier Triggered by dfaa5d09831607c3d4dea10b9ae2aaf15e95a298 on branch refs/heads/issue-114 --- .../lib/components/Statistics/Categories.tsx | 8 +++++--- .../js_src/lib/components/Statistics/hooks.tsx | 14 ++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx index 771e7e109d8..dda226fa192 100644 --- a/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx +++ b/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx @@ -53,9 +53,11 @@ function ItemOverride({ const noAccessTables: RA = React.useMemo( () => filterArray([ - // Dummy value to get the tables involved in the backend queries. Need this - // to show no permission when backend query fails due to permission denied - // The backend tables could be stored separately to avoid this + /* + * Dummy value to get the tables involved in the backend queries. Need this + * to show no permission when backend query fails due to permission denied + * The backend tables could be stored separately to avoid this + */ backEndSpecResolve?.querySpec?.(backEndSpecResolve.responseKey), dynamicSpecResolve?.dynamicQuerySpec, ]) diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx index 47236406e16..d219dadebd7 100644 --- a/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx +++ b/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx @@ -261,13 +261,13 @@ export function resolveStatsSpec( const pathToValue = item.pathToValue ?? statSpecItem.spec.pathToValue; return { type: 'BackEndStat', - pathToValue: pathToValue, + pathToValue, fetchUrl: statUrl, formatter: statSpecItem.spec.formatterGenerator(formatterSpec), querySpec: - pathToValue !== undefined - ? statSpecItem.spec.querySpec?.(pathToValue.toString()) - : undefined, + pathToValue === undefined + ? undefined + : statSpecItem.spec.querySpec?.(pathToValue.toString()), }; } if ( @@ -300,9 +300,11 @@ export function useResolvedStatSpec( const statsSpecRef = React.useRef>(undefined); if (rawStatsSpec !== undefined) { - if (rawStatsSpec.pathToValue !== undefined) + if (rawStatsSpec.pathToValue === undefined) { + statsSpecRef.current = undefined; + } else { statsSpecRef.current ??= rawStatsSpec; - else statsSpecRef.current = undefined; + } } return statsSpecRef.current ?? rawStatsSpec; From a373d00f7bc222a6ff3f08bbe29c50b799caaf3b Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:32:46 +0000 Subject: [PATCH 004/132] Lint code with ESLint and Prettier Triggered by 1359149a06fb2bca266f47e7a54db6379f7d8d79 on branch refs/heads/issue-114 --- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 4 ++-- .../frontend/js_src/lib/components/DataModel/resource.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 391941aaffe..fe1a1452421 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -1,5 +1,5 @@ -import { BusinessRuleResult } from './businessRules'; -import { AnySchema, TableFields } from './helperTypes'; +import type { BusinessRuleResult } from './businessRules'; +import type { AnySchema, TableFields } from './helperTypes'; import { checkPrepAvailability, getTotalLoaned, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resource.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resource.ts index 1197d2de2f8..e88ab595406 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resource.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resource.ts @@ -10,6 +10,7 @@ import { userPreferences } from '../Preferences/userPreferences'; import { formatUrl } from '../Router/queryString'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { addMissingFields } from './addMissingFields'; +import { getFieldsFromPath } from './businessRules'; import { serializeResource } from './helpers'; import type { AnySchema, @@ -21,7 +22,6 @@ import { getModel, schema } from './schema'; import type { SpecifyModel } from './specifyModel'; import type { Tables } from './types'; import { getUniquenessRules } from './uniquenessRules'; -import { getFieldsFromPath } from './businessRules'; // FEATURE: use this everywhere export const resourceEvents = eventListener<{ From 74502def697b5098e2cc1ee0c73aa5960ec6d0e6 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 26 Jun 2024 10:45:04 -0500 Subject: [PATCH 005/132] Allow modifying remote side of indedpenent resources via API --- specifyweb/specify/api.py | 41 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 4dfd4d4b88a..ae8972bb37e 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -601,17 +601,16 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: for field_name, val in list(data.items()): field = obj._meta.get_field(field_name) if not field.is_relation or (field.many_to_one or field.one_to_one): continue # Skip *-to-one fields. - - if isinstance(val, list): - assert isinstance(obj, models.Recordset) or obj.specify_model.get_field(field_name).dependent, \ - "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) - else: - # The field contains something other than nested data. - # Probably the URI of the collection of objects. - assert not obj.specify_model.get_field(field_name).dependent, \ - "didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val) - continue - + is_dependent = is_dependent_field(obj, field_name) + + if not isinstance(val, list): + if is_dependent: + raise AssertionError("didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val)) + else: + # The field contains something other than nested data. + # Probably the URI of the collection + continue + rel_model = field.related_model ids = [] # Ids not in this list will be deleted at the end. for rel_data in val: @@ -621,19 +620,27 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: rel_obj = update_obj(collection, agent, rel_model, rel_data['id'], rel_data['version'], rel_data, - parent_obj=obj) + parent_obj=obj if is_dependent_field(obj, field_name) else None) + else: # Create a new related object. rel_obj = create_obj(collection, agent, rel_model, rel_data, parent_obj=obj) + + if not is_dependent and not (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): + getattr(obj, field_name).add(rel_obj) ids.append(rel_obj.id) # Record the id as one to keep. # Delete related objects not in the ids list. # TODO: Check versions for optimistic locking. - to_delete = getattr(obj, field_name).exclude(id__in=ids) - for rel_obj in to_delete: - check_table_permissions(collection, agent, rel_obj, "delete") - auditlog.remove(rel_obj, agent, obj) - to_delete.delete() + to_remove = getattr(obj, field_name).exclude(id__in=ids).select_for_update() + if is_dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): + for rel_obj in to_remove: + check_table_permissions(collection, agent, rel_obj, "delete") + auditlog.remove(rel_obj, agent, obj) + + to_remove.delete() + else: + getattr(obj, field_name).remove(*list(to_remove)) @transaction.atomic def delete_resource(collection, agent, name, id, version) -> None: From a55de9ed95dc1c1a556def5c7ba42125a8dece62 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 26 Jun 2024 10:49:55 -0500 Subject: [PATCH 006/132] Add tests for inline independent resources --- specifyweb/permissions/permissions.py | 9 +-- specifyweb/specify/tests/test_api.py | 85 ++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/specifyweb/permissions/permissions.py b/specifyweb/permissions/permissions.py index 1fa6f90ec86..b01fdb38852 100644 --- a/specifyweb/permissions/permissions.py +++ b/specifyweb/permissions/permissions.py @@ -1,11 +1,10 @@ -from typing import Any, Callable, Tuple, List, Dict, Union, Iterable, Optional, NamedTuple +from typing import Any, Callable, Literal, List, Dict, Union, Iterable, Optional, NamedTuple import logging logger = logging.getLogger(__name__) from django.db import connection from django.db.models import Model -from django.core.exceptions import ObjectDoesNotExist from specifyweb.specify.models import Agent from specifyweb.specify.datamodel import Table @@ -172,7 +171,9 @@ def query(collectionid: Optional[int], userid: int, resource: str, action: str) ) -def check_table_permissions(collection, actor, obj, action: str) -> None: +TABLE_ACTION = Literal["read", "create", "update", "delete"] + +def check_table_permissions(collection, actor, obj, action: TABLE_ACTION) -> None: if isinstance(obj, Table): name = obj.name.lower() else: @@ -186,7 +187,7 @@ def check_field_permissions(collection, actor, obj, fields: Iterable[str], actio table = obj.specify_model.name.lower() enforce(collection, actor, [f'/field/{table}/{field}' for field in fields], action) -def table_permissions_checker(collection, actor, action: str) -> Callable[[Any], None]: +def table_permissions_checker(collection, actor, action: TABLE_ACTION) -> Callable[[Any], None]: def checker(obj) -> None: check_table_permissions(collection, actor, obj, action) return checker diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index b7568983f42..01a9558b198 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -518,7 +518,9 @@ def test_update_object_with_more_inlines(self): even_dets = [d for d in data['determinations'] if d['number1'] % 2 == 0] for d in even_dets: data['determinations'].remove(d) - data['collectionobjectattribute'] = {'text1': 'look! an attribute'} + text1_data = 'look! an attribute' + + data['collectionobjectattribute'] = {'text1': text1_data} api.update_obj(self.collection, self.agent, 'collectionobject', data['id'], data['version'], data) @@ -528,9 +530,88 @@ def test_update_object_with_more_inlines(self): for d in obj.determinations.all(): self.assertFalse(d.number1 % 2 == 0) - self.assertEqual(obj.collectionobjectattribute.text1, 'look! an attribute') + self.assertEqual(obj.collectionobjectattribute.text1, text1_data) + + def test_independent_set_inline(self): + accession_data = { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id), + 'collectionobjects': [ + api.obj_to_data(self.collectionobjects[0]) + ] + } + + accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) + self.collectionobjects[0].refresh_from_db() + self.assertEqual(accession, self.collectionobjects[0].accession) + + def test_indepenent_removing_from_inline(self): + accession = models.Accession.objects.create( + accessionnumber="a", + version="0", + division=self.division + ) + + accession.collectionobjects.set(self.collectionobjects) + + collection_objects_to_set = [self.collectionobjects[0], self.collectionobjects[3]] + + accession_data = { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id), + 'collectionobjects': [ + api.obj_to_data(collection_object) for collection_object in collection_objects_to_set + ] + } + accession = api.update_obj(self.collection, self.agent, 'Accession', accession.id, accession.version, accession_data) + + self.assertEqual(list(accession.collectionobjects.all()), collection_objects_to_set) + + # ensure the other CollectionObjects have not been deleted + self.assertEqual(len(models.Collectionobject.objects.all()), len(self.collectionobjects)) + + def test_updating_independent_resource(self): + co_to_modify = api.obj_to_data(self.collectionobjects[2]) + co_to_modify.update({ + 'integer1': 10, + 'determinations': [ + { + 'iscurrent': True, + 'collectionmemberid': self.collection.id, + 'collectionobject': api.uri_for_model('Collectionobject', self.collectionobjects[2].id) + } + ] + }) + accession_data = { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id), + 'collectionobjects': [ + co_to_modify + ] + } + + accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) + self.collectionobjects[2].refresh_from_db() + self.assertEqual(self.collectionobjects[2].integer1, 10) + self.assertEqual(len(self.collectionobjects[2].determinations.all()), 1) + + def test_independent_creating_from_remoteside(self): + new_catalognumber = f'num-{len(self.collectionobjects)}' + accession_data = { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id), + 'collectionobjects': [ + { + 'catalognumber': new_catalognumber, + 'collection': api.uri_for_model('Collection', self.collection.id) + } + ] + } + accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) + self.assertTrue(models.Collectionobject.objects.filter(catalognumber=new_catalognumber).exists()) + # version control on inlined resources should be tested From d970ca175f9f6f45330c74822134248e0ef7597e Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 27 Jun 2024 13:44:49 -0500 Subject: [PATCH 007/132] Write test for reassigning to-one relationship from to-many side --- specifyweb/specify/tests/test_api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index 01a9558b198..d1a6064b629 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -611,6 +611,26 @@ def test_independent_creating_from_remoteside(self): accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.assertTrue(models.Collectionobject.objects.filter(catalognumber=new_catalognumber).exists()) + + def test_reassigning_independent(self): + acc1 = models.Accession.objects.create( + accessionnumber="a", + division = self.division + ) + + self.collectionobjects[0].accession = acc1 + self.collectionobjects[0].save() + + accession_data = { + 'accessionnumber': "b", + 'division': api.uri_for_model('division', self.division.id), + 'collectionobjects': [ + api.obj_to_data(self.collectionobjects[0]) + ] + } + acc2 = api.create_obj(self.collection, self.agent, 'Accession', accession_data) + self.collectionobjects[0].refresh_from_db() + self.assertEqual(self.collectionobjects[0].accession, acc2) # version control on inlined resources should be tested From efd44b94be9ce1c7a1dd14f75d9f8c4654561207 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 3 Jul 2024 10:53:03 -0500 Subject: [PATCH 008/132] Modify frontend orm to handle indpendent to-many relationships --- .../lib/components/DataModel/collectionApi.ts | 5 +- .../lib/components/DataModel/resourceApi.ts | 186 +++++++++++++----- .../lib/components/DataModel/specifyTable.ts | 2 +- 3 files changed, 143 insertions(+), 50 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index f74ebf47166..15a29079a72 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -88,9 +88,10 @@ export const DependentCollection = Base.extend({ export const LazyCollection = Base.extend({ __name__: 'LazyCollectionBase', _neverFetched: true, - constructor(options = {}) { + constructor(options = {}, records = []) { this.table = this.model; - Base.call(this, null, options); + assert(_.isArray(records)); + Base.call(this, records, options); this.filters = options.filters || {}; this.domainfilter = Boolean(options.domainfilter) && diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 67b1c07982b..2356ee4ab84 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -4,12 +4,16 @@ import _ from 'underscore'; import { hijackBackboneAjax } from '../../utils/ajax/backboneAjax'; import { Http } from '../../utils/ajax/definitions'; +import { filterArray, ValueOf } from '../../utils/types'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; import { backboneFieldSeparator } from './helpers'; +import { AnySchema, SerializedResource, SerializedRecord } from './helperTypes'; +import { SpecifyResource } from './legacyTypes'; import { getFieldsToNotClone, getResourceApiUrl, @@ -18,7 +22,12 @@ import { resourceFromUrl, } from './resource'; import { initializeResource } from './scoping'; -import { specialFields } from './serializers'; +import { serializeResource, specialFields } from './serializers'; +import { LiteralField, Relationship } from './specifyField'; +import { Collection, SpecifyTable } from './specifyTable'; +import { Tables } from './types'; +import { IR } from '../../utils/types'; +import { tables } from './tables'; // REFACTOR: remove @ts-nocheck @@ -76,7 +85,19 @@ function eventHandlerForToMany(_related, field) { } // Always returns a resource -const maybeMakeResource = (value, relatedTable) => +const maybeMakeResource = < + TABLE extends SpecifyTable, + TABLE_SCHEMA extends Tables[TABLE['name']] +>( + value: + | SpecifyResource + | Partial< + | SerializedResource + | SerializedRecord + | { readonly id?: number } + >, + relatedTable: TABLE +): SpecifyResource => value instanceof ResourceBase ? value : new relatedTable.Resource(value, { parse: true }); @@ -100,6 +121,7 @@ export const ResourceBase = Backbone.Model.extend({ constructor() { this.specifyTable = this.constructor.specifyTable; this.dependentResources = {}; // References to related objects referred to by field in this resource + this.independentResources = {}; Reflect.apply(Backbone.Model, this, arguments); // TEST: check if this is necessary }, initialize(attributes, options) { @@ -211,7 +233,10 @@ export const ResourceBase = Backbone.Model.extend({ // Case insensitive return Backbone.Model.prototype.get.call(this, attribute.toLowerCase()); }, - storeDependent(field, related) { + storeDependent( + field: Relationship, + related: Collection | SpecifyResource | null + ): void { assert(field.isDependent()); const setter = field.type === 'one-to-many' @@ -219,7 +244,7 @@ export const ResourceBase = Backbone.Model.extend({ : '_setDependentToOne'; this[setter](field, related); }, - _setDependentToOne(field, related) { + _setDependentToOne(field: Relationship, related) { const oldRelated = this.dependentResources[field.name.toLowerCase()]; if (!related) { if (oldRelated) { @@ -255,7 +280,7 @@ export const ResourceBase = Backbone.Model.extend({ } } }, - _setDependentToMany(field, toMany) { + _setDependentToMany(field: Relationship, toMany: Collection) { const oldToMany = this.dependentResources[field.name.toLowerCase()]; oldToMany && oldToMany.off('all', null, this); @@ -263,6 +288,14 @@ export const ResourceBase = Backbone.Model.extend({ this.dependentResources[field.name.toLowerCase()] = toMany; toMany.on('all', eventHandlerForToMany(toMany, field), this); }, + storeIndependentToMany(field: Relationship, toMany: Collection) { + assert(!field.isDependent() && relationshipIsToMany(field)); + const oldToMany = this.independentResources[field.name.toLowerCase()]; + if (oldToMany !== undefined) oldToMany.off('all', null, this); + + this.independentResources[field.name.toLowerCase()] = toMany; + toMany.on('all', eventHandlerForToMany(toMany, field), this); + }, // Separate name to simplify typing bulkSet(attributes, options) { return this.set(attributes, options); @@ -383,7 +416,7 @@ export const ResourceBase = Backbone.Model.extend({ }, _handleInlineDataOrResource(value, fieldName) { // BUG: check type of value - const field = this.specifyTable.getField(fieldName); + const field: Relationship = this.specifyTable.getField(fieldName); const relatedTable = field.relatedTable; // BUG: don't do anything for virtual fields @@ -399,15 +432,23 @@ export const ResourceBase = Backbone.Model.extend({ ); this.storeDependent(field, collection); } else { - console.warn( - 'got unexpected inline data for independent collection field', - { collection: this, field, value } + const collection = new relatedTable.ToOneCollection( + collectionOptions, + value ); + this.storeIndependentToMany(field, collection); } // Because the foreign key is on the other side this.trigger(`change:${fieldName}`, this); this.trigger('change', this); + + /** + * These are serialized and added to the JSON before being sent to the + * server and are not in the resource's attributes + * + * https://backbonejs.org/#Sync + */ return undefined; } case 'many-to-one': { @@ -533,9 +574,13 @@ export const ResourceBase = Backbone.Model.extend({ return value; }); }, - async _rget(path, options) { + async _rget( + path: RA, + options: OPTIONS + ) { let fieldName = path[0].toLowerCase(); - const field = this.specifyTable.getField(fieldName); + const field: LiteralField | Relationship | undefined = + this.specifyTable.getField(fieldName); field && (fieldName = field.name.toLowerCase()); // In case fieldName is an alias let value = this.get(fieldName); field || @@ -588,41 +633,9 @@ export const ResourceBase = Backbone.Model.extend({ throw "can't traverse into a collection using dot notation"; } - // Is the collection cached? - let toMany = this.dependentResources[fieldName]; - if (!toMany) { - const collectionOptions = { - field: field.getReverse(), - related: this, - }; - - if (!field.isDependent()) { - return new related.ToOneCollection(collectionOptions); - } - - if (this.isNew()) { - toMany = new related.DependentCollection(collectionOptions, []); - this.storeDependent(field, toMany); - return toMany; - } else { - console.warn('expected dependent resource to be in cache'); - const temporaryCollection = new related.ToOneCollection( - collectionOptions - ); - return temporaryCollection - .fetch({ limit: 0 }) - .then( - () => - new related.DependentCollection( - collectionOptions, - temporaryCollection.tables - ) - ) - .then((toMany) => { - _this.storeDependent(field, toMany); - }); - } - } + return field.isDependent() + ? this.getDependentToMany(field) + : this.getIndependentToMany(field); } case 'zero-to-one': { /* @@ -664,7 +677,77 @@ export const ResourceBase = Backbone.Model.extend({ } } }, - save({ + async getDependentToMany( + field: Relationship + ): Promise> { + assert(field.isDependent()); + + const fieldName = field.name.toLowerCase(); + const relatedTable = field.relatedTable; + + let toMany = this.dependentResources[fieldName]; + + if (toMany) return toMany; + + const collectionOptions = { + field: field.getReverse(), + related: this, + }; + + if (this.isNew()) { + toMany = new relatedTable.DependentCollection(collectionOptions, []); + this.storeDependent(field, toMany); + return toMany; + } else { + console.warn('expected dependent resource to be in cache'); + const temporaryCollection = new relatedTable.ToOneCollection( + collectionOptions + ); + return temporaryCollection + .fetch({ limit: 0 }) + .then( + () => + new relatedTable.DependentCollection( + collectionOptions, + temporaryCollection.tables + ) + ) + .then((toMany) => { + _this.storeDependent(field, toMany); + }); + } + }, + async getIndependentToMany( + field: Relationship + ): Promise> { + assert(!field.isDependent()); + + const fieldName = field.name.toLowerCase(); + const relatedTable = field.relatedTable; + + let toMany: Collection = this.independentResources[fieldName]; + + if (toMany) return toMany; + + const collectionOptions = { + field: field.getReverse(), + related: this, + }; + + if (this.isNew()) { + toMany = new relatedTable.ToOneCollection(collectionOptions); + this.storeIndependentToMany(field, toMany); + return toMany; + } + + return new relatedTable.ToOneCollection(collectionOptions) + .fetch({ limit: 0 }) + .then((collection) => { + this.storeIndependentToMany(field, collection); + return collection; + }); + }, + async save({ onSaveConflict: handleSaveConflict, errorOnAlreadySaving = true, } = {}) { @@ -730,6 +813,15 @@ export const ResourceBase = Backbone.Model.extend({ json[fieldName] = related ? related.toJSON() : null; } }); + + Object.entries(self.independentResources).forEach( + ([fieldName, related]) => { + const field = self.specifyTable.getField(fieldName); + if (field.type === 'one-to-many' && related) { + json[fieldName] = related.toJSON(); + } + } + ); if (typeof this.get('resource_uri') !== 'string') json._tableName = this.specifyTable.name; return json; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 8ce8576e7a3..36c7b1de01e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -73,7 +73,7 @@ type CollectionConstructor = new ( >; readonly domainfilter?: boolean; }, - tables?: RA> + initalResources?: RA> ) => UnFetchedCollection; export type UnFetchedCollection = { From f2a439791d1648394b8f5941aaccea0b72cc1f0b Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 25 Jul 2024 10:28:03 -0500 Subject: [PATCH 009/132] Add Independent Collection to Collection API --- .../lib/components/DataModel/collectionApi.ts | 74 ++++++++++++++++++- .../lib/components/DataModel/resourceApi.ts | 6 +- .../lib/components/DataModel/specifyTable.ts | 8 ++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 15a29079a72..02692b7a657 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -85,13 +85,81 @@ export const DependentCollection = Base.extend({ create: notSupported, }); +export const IndependentCollection = Base.extend({ + __name__: 'IndependentCollectionBase', + constructor(options, records = []) { + this.table = this.model; + assert(_.isArray(records)); + Base.call(this, records, options); + this.filters = options.filters || {}; + this.domainfilter = + Boolean(options.domainfilter) && + this.model?.specifyTable.getScopingRelationship() !== undefined; + }, + initialize(_tables, options) { + this.on( + 'add remove', + function () { + /* + * Warning: changing a collection record does not trigger a + * change event in the parent (though it probably should) + */ + this.trigger('saverequired'); + }, + this + ); + + setupToOne(this, options); + }, + url() { + return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; + }, + parse(resp) { + let objects; + if (resp.meta) { + objects = resp.objects; + } else { + console.warn("expected 'meta' in response"); + objects = resp; + } + + return objects; + }, + async fetch(options) { + if (this.related.isNew()) { + return this; + } + + this.filters[this.field.name.toLowerCase()] = this.related.id; + if (this._fetch) return this._fetch; + + options ||= {}; + + options.update = true; + options.remove = false; + options.silent = true; + assert(options.at == null); + + options.data = + options.data || + _.extend({ domainfilter: this.domainfilter }, this.filters); + options.data.offset = this.length; + + _(options).has('limit') && (options.data.limit = options.limit); + this._fetch = Backbone.Collection.prototype.fetch.call(this, options); + return this._fetch.then(() => { + this._fetch = null; + return this; + }); + }, +}); + export const LazyCollection = Base.extend({ __name__: 'LazyCollectionBase', _neverFetched: true, - constructor(options = {}, records = []) { + constructor(options = {}) { this.table = this.model; - assert(_.isArray(records)); - Base.call(this, records, options); + Base.call(this, null, options); this.filters = options.filters || {}; this.domainfilter = Boolean(options.domainfilter) && diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 3e75eeecbee..a7e346afa90 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -434,7 +434,7 @@ export const ResourceBase = Backbone.Model.extend({ ); this.storeDependent(field, collection); } else { - const collection = new relatedTable.ToOneCollection( + const collection = new relatedTable.IndependentCollection( collectionOptions, value ); @@ -737,12 +737,12 @@ export const ResourceBase = Backbone.Model.extend({ }; if (this.isNew()) { - toMany = new relatedTable.ToOneCollection(collectionOptions); + toMany = new relatedTable.IndependentCollection(collectionOptions); this.storeIndependentToMany(field, toMany); return toMany; } - return new relatedTable.ToOneCollection(collectionOptions) + return new relatedTable.IndependentCollection(collectionOptions) .fetch({ limit: 0 }) .then((collection) => { this.storeIndependentToMany(field, collection); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 36c7b1de01e..b8dc99894ef 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -16,6 +16,7 @@ import { parentTableRelationship } from '../Forms/parentTables'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { DependentCollection, + IndependentCollection, LazyCollection, ToOneCollection, } from './collectionApi'; @@ -177,6 +178,8 @@ export class SpecifyTable { */ public readonly DependentCollection: CollectionConstructor; + public readonly IndependentCollection: CollectionConstructor; + /** * A Backbone collection for loading a collection of items of this type as a * backwards -to-one collection of some other resource. @@ -235,6 +238,11 @@ export class SpecifyTable { model: this.Resource, }); + this.IndependentCollection = IndependentCollection.extend({ + __name__: `${this.name}IndependentCollection`, + model: this.Resource, + }); + this.ToOneCollection = ToOneCollection.extend({ __name__: `${this.name}ToOneCollection`, model: this.Resource, From e63365cae95c9fbcaa48bd440dfb89fd2da78132 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 25 Jul 2024 21:01:38 -0500 Subject: [PATCH 010/132] Recompute resource delete blockers on save --- .../js_src/lib/components/Forms/DeleteButton.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx b/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx index 3d0a7c8bf4f..f6706b4e45f 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx @@ -17,6 +17,7 @@ import { icons } from '../Atoms/Icons'; import { LoadingContext } from '../Core/Contexts'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { resourceOn } from '../DataModel/resource'; import { serializeResource } from '../DataModel/serializers'; import type { Relationship } from '../DataModel/specifyField'; import { strictGetTable } from '../DataModel/tables'; @@ -71,6 +72,19 @@ export function DeleteButton({ false ); + React.useEffect( + () => + deferred + ? undefined + : resourceOn( + resource, + 'saved', + () => void fetchBlockers(resource).then(setBlockers), + false + ), + [resource, deferred] + ); + const [isOpen, handleOpen, handleClose] = useBooleanState(); const loading = React.useContext(LoadingContext); From 5b41b023796fe0827d4ede53c64ef740eeca0f58 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 25 Jul 2024 21:42:36 -0500 Subject: [PATCH 011/132] Don't default independent subviews as buttons --- .../frontend/js_src/lib/components/Forms/SubView.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index e0fdb77cf1b..2c675d7ef66 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -41,7 +41,7 @@ export function SubView({ parentResource, parentFormType, formType: initialFormType, - isButton: rawIsButton, + isButton, viewName = relationship.relatedTable.view, icon = relationship.relatedTable.name, sortField: initialSortField, @@ -190,8 +190,6 @@ export function SubView({ const isReadOnly = React.useContext(ReadOnlyContext); - // See https://github.com/specify/specify7/issues/3127 - const isButton = rawIsButton || (!isReadOnly && !relationship.isDependent()); const [isOpen, _, handleClose, handleToggle] = useBooleanState(!isButton); const [isAttachmentConfigured] = usePromise(attachmentSettingsPromise, true); @@ -238,11 +236,7 @@ export function SubView({ )} {typeof collection === 'object' && isOpen ? ( Date: Thu, 25 Jul 2024 21:52:57 -0500 Subject: [PATCH 012/132] Propagate save blockers on independent resources to related record This is not likely the behavior we want, so this is something to be discussed. See related #3127 --- .../lib/components/DataModel/legacyTypes.ts | 4 ++ .../lib/components/DataModel/saveBlockers.tsx | 41 +++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts index bf0d9ee2289..46b65d3b3df 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts @@ -156,6 +156,10 @@ export type SpecifyResource = { ): SpecifyResource; // Not type safe bulkSet(value: IR): SpecifyResource; + //Unsafe + readonly independentResources: IR< + Collection | SpecifyResource | null | undefined + >; // Unsafe. Use getDependentResource instead whenever possible readonly dependentResources: IR< Collection | SpecifyResource | null | undefined diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx index 117f0e085e0..f8e982124f3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx @@ -200,28 +200,25 @@ const getAllBlockers = ( resources: [resource], })) ?? []), ...filterArray( - Object.entries(resource.dependentResources).flatMap( - ([fieldName, collectionOrResource]) => - (filterBlockers !== undefined && - fieldName.toLowerCase() !== filterBlockers?.name.toLowerCase()) || - collectionOrResource === undefined || - collectionOrResource === null - ? undefined - : (collectionOrResource instanceof ResourceBase - ? getAllBlockers( - collectionOrResource as SpecifyResource - ) - : (collectionOrResource as Collection).models.flatMap( - f.unary(getAllBlockers) - ) - ).map(({ field, resources, message }) => ({ - field: [ - resource.specifyTable.strictGetField(fieldName), - ...field, - ], - resources: [...resources, resource], - message, - })) + Object.entries({ + ...resource.dependentResources, + ...resource.independentResources, + }).flatMap(([fieldName, collectionOrResource]) => + (filterBlockers !== undefined && + fieldName.toLowerCase() !== filterBlockers?.name.toLowerCase()) || + collectionOrResource === undefined || + collectionOrResource === null + ? undefined + : (collectionOrResource instanceof ResourceBase + ? getAllBlockers(collectionOrResource as SpecifyResource) + : (collectionOrResource as Collection).models.flatMap( + f.unary(getAllBlockers) + ) + ).map(({ field, resources, message }) => ({ + field: [resource.specifyTable.strictGetField(fieldName), ...field], + resources: [...resources, resource], + message, + })) ) ), ]; From 59d1ff86a7c958bf4e52b8c7560f5c2d7fb3ce7e Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 09:45:24 -0500 Subject: [PATCH 013/132] Add CO -> isMemberOfCOG virtual field to frontend --- .../js_src/lib/components/DataModel/schemaExtras.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts index 6fbde1459ac..c073c863c11 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts @@ -83,6 +83,14 @@ export const schemaExtras: { indexed: false, unique: false, }), + new LiteralField(table, { + name: 'isMemberOfCOG', + required: false, + readOnly: true, + type: 'java.lang.Boolean', + indexed: false, + unique: false, + }), ], (): void => { const collection = getField(table, 'collection'); From 4abacf0e4a904431768ef83564727198c72b4e9f Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 09:46:59 -0500 Subject: [PATCH 014/132] Improve date backendfilters on frontend --- .../js_src/lib/components/DataModel/helpers.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts index 8ccdfdb534b..3e04d2b4c96 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts @@ -1,5 +1,6 @@ import { f } from '../../utils/functools'; import type { RA, ValueOf } from '../../utils/types'; +import { caseInsensitiveHash } from '../../utils/utils'; import { isTreeResource } from '../InitialContext/treeRanks'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { @@ -40,7 +41,7 @@ const weekDayMap = { Thursday: 5, Friday: 6, Saturday: 7, -}; +} as const; /** * Use this to construct a query using a lookup for Django. @@ -99,7 +100,7 @@ export const backendFilter = (field: string) => ({ [[field, 'day'].join(djangoLookupSeparator)]: value, }), monthEquals: (value: number) => ({ - [[field, 'lte'].join(djangoLookupSeparator)]: value, + [[field, 'month'].join(djangoLookupSeparator)]: value, }), yearEquals: (value: number) => ({ [[field, 'year'].join(djangoLookupSeparator)]: value, @@ -107,8 +108,13 @@ export const backendFilter = (field: string) => ({ weekEquals: (value: number) => ({ [[field, 'week'].join(djangoLookupSeparator)]: value, }), - weekDayEquals: (value: keyof typeof weekDayMap) => ({ - [[field, 'week_day'].join(djangoLookupSeparator)]: weekDayMap[value], + weekDayEquals: ( + value: ValueOf | keyof typeof weekDayMap + ) => ({ + [[field, 'week_day'].join(djangoLookupSeparator)]: + typeof value === 'number' + ? value + : caseInsensitiveHash(weekDayMap, value), }), }); From dc570f65ebd9c79ec34937dde49987ec15189edf Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 09:50:33 -0500 Subject: [PATCH 015/132] Allow creating new records in Independent ToMany Collections --- .../lib/components/DataModel/collectionApi.ts | 68 +++++++++++++----- .../lib/components/DataModel/resourceApi.ts | 51 ++++++------- .../lib/components/DataModel/saveBlockers.tsx | 7 +- .../lib/components/FormCells/FormTable.tsx | 71 ++++++++----------- .../FormSliders/IntegratedRecordSelector.tsx | 19 ++++- .../components/FormSliders/RecordSelector.tsx | 61 +++++++++------- .../RecordSelectorFromCollection.tsx | 5 +- .../js_src/lib/components/Forms/SubView.tsx | 11 ++- .../lib/components/SearchDialog/index.tsx | 50 +++++++++++-- .../WorkBench/useDisambiguationDialog.tsx | 3 +- specifyweb/specify/api.py | 31 ++++++-- specifyweb/specify/tests/test_api.py | 52 +++++++++++++- 12 files changed, 293 insertions(+), 136 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 02692b7a657..34371d71228 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -1,9 +1,13 @@ // @ts-nocheck import _ from 'underscore'; +import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; +import { formatUrl } from '../Router/queryString'; import { Backbone } from './backbone'; +import { AnySchema } from './helperTypes'; +import { SpecifyResource } from './legacyTypes'; // REFACTOR: remove @ts-nocheck @@ -14,6 +18,10 @@ const Base = Backbone.Collection.extend({ }, }); +export const isRelationshipCollection = (value: unknown): boolean => + value instanceof DependentCollection || + value instanceof IndependentCollection; + function notSupported() { throw new Error('method is not supported'); } @@ -95,15 +103,25 @@ export const IndependentCollection = Base.extend({ this.domainfilter = Boolean(options.domainfilter) && this.model?.specifyTable.getScopingRelationship() !== undefined; + + this.changedResources = Object.fromEntries( + records.map((record) => [record.cid, false]) + ); }, initialize(_tables, options) { + this.on( + 'change', + function (resource: SpecifyResource) { + if (!resource.isBeingInitialized()) + this.changedResources[resource.cid] = true; + this.trigger('saverequired'); + }, + this + ); this.on( 'add remove', - function () { - /* - * Warning: changing a collection record does not trigger a - * change event in the parent (though it probably should) - */ + function (resource: SpecifyResource) { + this.changedResources[resource.cid] = false; this.trigger('saverequired'); }, this @@ -115,15 +133,14 @@ export const IndependentCollection = Base.extend({ return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; }, parse(resp) { - let objects; + let records; if (resp.meta) { - objects = resp.objects; + records = resp.objects; } else { console.warn("expected 'meta' in response"); - objects = resp; + records = resp; } - - return objects; + return records; }, async fetch(options) { if (this.related.isNew()) { @@ -133,25 +150,38 @@ export const IndependentCollection = Base.extend({ this.filters[this.field.name.toLowerCase()] = this.related.id; if (this._fetch) return this._fetch; - options ||= {}; + options = { ...(options ?? {}), silent: true }; - options.update = true; - options.remove = false; - options.silent = true; assert(options.at == null); - options.data = - options.data || - _.extend({ domainfilter: this.domainfilter }, this.filters); - options.data.offset = this.length; + options.data = options.data || { + ...this.filters, + domainfilter: this.domainfilter, + limit: options.limit, + }; - _(options).has('limit') && (options.data.limit = options.limit); this._fetch = Backbone.Collection.prototype.fetch.call(this, options); return this._fetch.then(() => { this._fetch = null; return this; }); }, + isComplete() { + return true; + }, + toApiJSON(options) { + const self = this; + const resources = + Object.keys(self.changedResources).length === 0 + ? formatUrl(this.url(), this.filters) + : this.map(function (resource) { + return self.changedResources[resource.cid] === true + ? resource.toJSON(options) + : resource.url(); + }); + this.changedResources = []; + return resources; + }, }); export const LazyCollection = Base.extend({ diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 3d69ca9827e..24eee2f73dc 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -4,16 +4,20 @@ import _ from 'underscore'; import { hijackBackboneAjax } from '../../utils/ajax/backboneAjax'; import { Http } from '../../utils/ajax/definitions'; -import { filterArray, ValueOf } from '../../utils/types'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; +import { isRelationshipCollection } from './collectionApi'; import { backboneFieldSeparator } from './helpers'; -import { AnySchema, SerializedResource, SerializedRecord } from './helperTypes'; -import { SpecifyResource } from './legacyTypes'; +import type { + AnySchema, + SerializedRecord, + SerializedResource, +} from './helperTypes'; +import type { SpecifyResource } from './legacyTypes'; import { getFieldsToNotClone, getResourceApiUrl, @@ -22,12 +26,10 @@ import { resourceFromUrl, } from './resource'; import { initializeResource } from './scoping'; -import { serializeResource, specialFields } from './serializers'; -import { LiteralField, Relationship } from './specifyField'; -import { Collection, SpecifyTable } from './specifyTable'; -import { Tables } from './types'; -import { IR } from '../../utils/types'; -import { tables } from './tables'; +import { specialFields } from './serializers'; +import type { LiteralField, Relationship } from './specifyField'; +import type { Collection, SpecifyTable } from './specifyTable'; +import type { Tables } from './types'; // REFACTOR: remove @ts-nocheck @@ -577,6 +579,11 @@ export const ResourceBase = Backbone.Model.extend({ if (!value) return value; // Ok if the related resource doesn't exist else if (typeof value.fetchIfNotPopulated === 'function') return value.fetchIfNotPopulated(); + /* + * Relationship Collections have already been fetched through _rget. + * This is needed to prevent refetching the collection with the default + * limit of 20 + */ else if (isRelationshipCollection(value)) return value; else if (typeof value.fetch === 'function') return value.fetch(); } return value; @@ -733,27 +740,23 @@ export const ResourceBase = Backbone.Model.extend({ const fieldName = field.name.toLowerCase(); const relatedTable = field.relatedTable; - let toMany: Collection = this.independentResources[fieldName]; - - if (toMany) return toMany; + const existingToMany: Collection | undefined = + this.independentResources[fieldName]; const collectionOptions = { field: field.getReverse(), related: this, }; - if (this.isNew()) { - toMany = new relatedTable.IndependentCollection(collectionOptions); - this.storeIndependentToMany(field, toMany); - return toMany; - } + const collection = + existingToMany === undefined + ? new relatedTable.IndependentCollection(collectionOptions) + : existingToMany; - return new relatedTable.IndependentCollection(collectionOptions) - .fetch({ limit: 0 }) - .then((collection) => { - this.storeIndependentToMany(field, collection); - return collection; - }); + return collection.fetch({ limit: 0 }).then((collection) => { + this.storeIndependentToMany(field, collection); + return collection; + }); }, async save({ onSaveConflict: handleSaveConflict, @@ -826,7 +829,7 @@ export const ResourceBase = Backbone.Model.extend({ ([fieldName, related]) => { const field = self.specifyTable.getField(fieldName); if (field.type === 'one-to-many' && related) { - json[fieldName] = related.toJSON(); + json[fieldName] = related.toApiJSON(); } } ); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx index f8e982124f3..28501c12dc2 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx @@ -9,6 +9,7 @@ import { eventListener } from '../../utils/events'; import { f } from '../../utils/functools'; import type { GetOrSet, RA } from '../../utils/types'; import { filterArray } from '../../utils/types'; +import type { SET } from '../../utils/utils'; import { removeItem } from '../../utils/utils'; import { softError } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; @@ -108,10 +109,10 @@ export function useSaveBlockers( ]; } -export function setSaveBlockers( - resource: SpecifyResource, +export function setSaveBlockers( + resource: SpecifyResource, field: LiteralField | Relationship, - errors: Parameters>[1]>[0], + errors: Parameters>[typeof SET]>[0], blockerKey: string ): void { const resolvedErrors = diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index e58d70ba633..acc11b08d04 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; -import type { State } from 'typesafe-reducer'; import { useId } from '../../hooks/useId'; import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'; @@ -31,7 +30,7 @@ import type { SortConfig } from '../Molecules/Sorting'; import { SortIndicator } from '../Molecules/Sorting'; import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; -import { SearchDialog } from '../SearchDialog'; +import { SearchDialog, useSearchDialog } from '../SearchDialog'; import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { FormCell } from './index'; @@ -173,9 +172,6 @@ export function FormTable({ const [isExpanded, setExpandedRecords] = React.useState< IR >({}); - const [state, setState] = React.useState< - State<'MainState'> | State<'SearchState'> - >({ type: 'MainState' }); const [flexibleColumnWidth] = userPreferences.use( 'form', 'definition', @@ -198,6 +194,14 @@ export function FormTable({ const [maxHeight] = userPreferences.use('form', 'formTable', 'maxHeight'); + const { searchDialog, showSearchDialog } = useSearchDialog({ + forceCollection: undefined, + extraFilters: undefined, + table: relationship.relatedTable, + multiple: !isToOne, + onSelected: handleAddResources, + }); + const children = collapsedViewDefinition === undefined ? ( commonText.loading() @@ -387,7 +391,9 @@ export function FormTable({ )}
- {displayViewButton && isExpanded[resource.cid] === true ? ( + {displayViewButton && + isExpanded[resource.cid] === true && + !resource.isNew() ? ( ({
); - const addButton = + const addButtons = typeof handleAddResources === 'function' && mode !== 'view' && - !disableAdding && - hasTablePermission( - relationship.relatedTable.name, - isDependent ? 'create' : 'read' - ) ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - } - : (): void => - setState({ - type: 'SearchState', - }) - } - /> + !disableAdding ? ( + <> + {!isDependent && + hasTablePermission(relationship.relatedTable.name, 'read') ? ( + + ) : undefined} + {hasTablePermission(relationship.relatedTable.name, 'create') ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + }} + /> + ) : undefined} + ) : undefined; return dialog === false ? ( {preHeaderButtons} {header} - {addButton} + {addButtons} {children} - {state.type === 'SearchState' && - typeof handleAddResources === 'function' ? ( - setState({ type: 'MainState' })} - onSelected={handleAddResources} - /> - ) : undefined} + {searchDialog} ) : ( diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 8e594d2a0a6..120c896eba6 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -120,7 +120,7 @@ export function IntegratedRecordSelector({ collection={collection} defaultIndex={isToOne ? 0 : index} relationship={relationship} - onAdd={(resources) => { + onAdd={(resources): void => { if (isInteraction) { setInteractionResource(resources[0]); handleOpenDialog(); @@ -145,6 +145,7 @@ export function IntegratedRecordSelector({ resource, onAdd: handleAdd, onRemove: handleRemove, + showSearchDialog, isLoading, }): JSX.Element => ( <> @@ -178,9 +179,23 @@ export function IntegratedRecordSelector({ !isDependent && dialog === false ? resource : undefined } /> + {!isDependent && + hasTablePermission( + relationship.relatedTable.name, + 'read' + ) && + typeof handleAdd === 'function' ? ( + 0) + } + onClick={showSearchDialog} + /> + ) : undefined} {hasTablePermission( relationship.relatedTable.name, - isDependent ? 'create' : 'read' + 'create' ) && typeof handleAdd === 'function' ? ( = { @@ -56,6 +56,7 @@ export type RecordSelectorState = { readonly onRemove: | ((source: 'deleteButton' | 'minusButton') => void) | undefined; + readonly showSearchDialog: () => void; // True while fetching new record readonly isLoading: boolean; }; @@ -82,9 +83,33 @@ export function useRecordSelector({ [index] ); - const [state, setState] = React.useState< - State<'AddBySearch'> | State<'Main'> - >({ type: 'Main' }); + const isToOne = !relationshipIsToMany(field) || field?.type === 'zero-to-one'; + + const handleResourcesSelected = React.useMemo( + () => + typeof handleAdded === 'function' + ? (resources: RA>): void => { + if (field?.isDependent() ?? true) + f.maybe(field?.otherSideName, (fieldName) => + f.maybe(relatedResource?.url(), (url) => + resources.forEach((resource) => { + resource.set(fieldName, url as never); + }) + ) + ); + handleAdded(resources); + } + : undefined, + [handleAdded, relatedResource, field] + ); + + const { searchDialog, showSearchDialog } = useSearchDialog({ + extraFilters: undefined, + forceCollection: undefined, + multiple: !isToOne, + table, + onSelected: handleResourcesSelected, + }); return { slider: ( @@ -94,7 +119,7 @@ export function useRecordSelector({ onChange={ handleSlide === undefined ? undefined - : (index) => handleSlide?.(index, false) + : (index): void => handleSlide?.(index, false) } /> ), @@ -103,26 +128,7 @@ export function useRecordSelector({ isLoading: records[index] === undefined && totalCount !== 0, // While new resource is loading, display previous resource resource: records[index] ?? records[lastIndexRef.current], - dialogs: - state.type === 'AddBySearch' && typeof handleAdded === 'function' ? ( - setState({ type: 'Main' })} - onSelected={(resources): void => { - f.maybe(field?.otherSideName, (fieldName) => - f.maybe(relatedResource?.url(), (url) => - resources.forEach((resource) => - resource.set(fieldName, url as never) - ) - ) - ); - handleAdded(resources); - }} - /> - ) : null, + dialogs: searchDialog, onAdd: typeof handleAdded === 'function' ? (resources: RA>): void => { @@ -134,7 +140,7 @@ export function useRecordSelector({ ) resource.set(field.otherSideName, relatedResource.url() as any); handleAdded([resource]); - } else setState({ type: 'AddBySearch' }); + } else showSearchDialog(); } : undefined, onRemove: @@ -156,5 +162,6 @@ export function useRecordSelector({ ) : undefined : undefined, + showSearchDialog, }; } diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 76485597dfd..7c72ed933e5 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -63,7 +63,7 @@ export function RecordSelectorFromCollection({ () => resourceOn( collection, - 'add remove destroy', + 'add remove destroy sync', (): void => setRecords(getRecords), true ), @@ -94,8 +94,9 @@ export function RecordSelectorFromCollection({ ...rest, index, table: collection.table.specifyTable, + field: relationship, records, - relatedResource: isDependent ? collection.related : undefined, + relatedResource: isLazy ? undefined : collection.related, totalCount: collection._totalCount ?? records.length, onAdd: (rawResources): void => { const resources = isToOne ? rawResources.slice(0, 1) : rawResources; diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 2c675d7ef66..51f181452e2 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -4,7 +4,7 @@ import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; -import { overwriteReadOnly } from '../../utils/types'; +import { overwriteReadOnly, RA } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { attachmentSettingsPromise } from '../Attachments/attachments'; @@ -118,10 +118,9 @@ export function SubView({ related: parentResource, field: reverse, }) - : new relationship.relatedTable.LazyCollection({ - filters: { - [reverse.name]: parentResource.id, - }, + : new relationship.relatedTable.IndependentCollection({ + related: parentResource, + field: reverse, }) ) as Collection; if (relationship.isDependent() && parentResource.isNew()) @@ -154,7 +153,7 @@ export function SubView({ () => resourceOn( parentResource, - `change:${relationship.name}`, + `change:${relationship.name} saved`, (): void => { versionRef.current += 1; const localVersionRef = versionRef.current; diff --git a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx index 96da3cb751f..bbb48929f9e 100644 --- a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; +import type { State } from 'typesafe-reducer'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; @@ -49,10 +50,7 @@ const viewNameExceptions: Partial> = { GeologicTimePeriod: 'ChronosStratSearch', }; -/** - * Display a resource search dialog - */ -export function SearchDialog(props: { +type SearchDialogProps = { readonly forceCollection: number | undefined; readonly extraFilters: RA> | undefined; readonly table: SpecifyTable; @@ -61,7 +59,14 @@ export function SearchDialog(props: { readonly searchView?: string; readonly onSelected: (resources: RA>) => void; readonly onlyUseQueryBuilder?: boolean; -}): JSX.Element | null { +}; + +/** + * Display a resource search dialog + */ +export function SearchDialog( + props: SearchDialogProps +): JSX.Element | null { const [alwaysUseQueryBuilder] = userPreferences.use( 'form', 'queryComboBox', @@ -84,6 +89,41 @@ export function SearchDialog(props: { ); } +/** + * Displays a SearchDialog whenever `showSearchDialog` is invoked + */ +export function useSearchDialog({ + onSelected: handleSelected, + onClose: handleClosed, + ...rest +}: Omit, 'onClose' | 'onSelected'> & + Partial, 'onClose' | 'onSelected'>>): { + readonly searchDialog: JSX.Element | null; + readonly showSearchDialog: () => void; +} { + const [state, setState] = React.useState | State<'Search'>>({ + type: 'Main', + }); + + return { + searchDialog: + state.type === 'Search' && typeof handleSelected === 'function' ? ( + { + handleClosed?.(); + setState({ type: 'Main' }); + }} + onSelected={handleSelected} + /> + ) : null, + showSearchDialog: () => + typeof handleSelected === 'function' + ? setState({ type: 'Search' }) + : undefined, + }; +} + const filterResults = ( results: RA>, extraFilters: RA> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index 6272bb7090b..04478d71858 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -6,6 +6,7 @@ import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { type RA } from '../../utils/types'; import { LoadingContext } from '../Core/Contexts'; +import { backendFilter } from '../DataModel/helpers'; import type { AnySchema } from '../DataModel/helperTypes'; import type { Collection } from '../DataModel/specifyTable'; import { strictGetTable } from '../DataModel/tables'; @@ -76,7 +77,7 @@ export function useDisambiguationDialog({ ); const table = strictGetTable(tableName); const resources = new table.LazyCollection({ - filters: { id__in: matches.ids.join(',') }, + filters: backendFilter('id').isIn(matches.ids), }) as Collection; loading( diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 527bd2637d1..1fd30ce3c74 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -594,9 +594,7 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List elif isinstance(val, str): # The related object is given by a URI reference. assert not dependent, "didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val) - fk_model, fk_id = parse_uri(val) - assert fk_model == field.related_model.__name__.lower() - assert fk_id is not None + fk_model, fk_id = strict_uri_to_model(val, field.related_model.__name__) setattr(obj, field_name, get_object_or_404(fk_model, id=fk_id)) new_related_id = fk_id @@ -653,8 +651,27 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: continue rel_model = field.related_model - ids = [] # Ids not in this list will be deleted at the end. + ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. + + ids_to_fetch = [] + cached_objs = dict() + fk_model = None + if not is_dependent: + for rel_data in val: + if not isinstance(rel_data, str): continue + fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) + ids_to_fetch.append(fk_id) + + if fk_model is not None: + cached_objs = {item.id: obj_to_data(item) for item in get_model(fk_model).objects.filter(id__in=ids_to_fetch)} + for rel_data in val: + if isinstance(rel_data, str): + assert not is_dependent, "expected object for dependent field %s in %s: %s" % (field_name, obj, rel_data) + fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) + rel_data = cached_objs[fk_id] + + #FIXME: only update related objects when they change rel_data[field.field.name] = obj if 'id' in rel_data: # Update an existing related object. @@ -788,6 +805,12 @@ def parse_uri(uri: str) -> Tuple[str, str]: groups = match.groups() return groups[0], groups[2] +def strict_uri_to_model(uri: str, model: str) -> Tuple[str, int]: + uri_model, uri_id = parse_uri(uri) + assert model.lower() == uri_model.lower() + assert uri_id is not None + return uri_model, int(uri_id) + def obj_to_data(obj) -> Dict[str, Any]: "Wrapper for backwards compat w/ other modules that use this function." # TODO: Such functions should be audited for whether they should apply diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index b96b9094df3..473d31ab12e 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -499,13 +499,16 @@ def test_independent_set_inline(self): 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), 'collectionobjects': [ - api.obj_to_data(self.collectionobjects[0]) + api.obj_to_data(self.collectionobjects[0]), + api.uri_for_model('collectionobject', self.collectionobjects[1].id) ] } accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.collectionobjects[0].refresh_from_db() + self.collectionobjects[1].refresh_from_db() self.assertEqual(accession, self.collectionobjects[0].accession) + self.assertEqual(accession, self.collectionobjects[1].accession) def test_indepenent_removing_from_inline(self): accession = models.Accession.objects.create( @@ -516,13 +519,17 @@ def test_indepenent_removing_from_inline(self): accession.collectionobjects.set(self.collectionobjects) + self.assertEqual(accession, self.collectionobjects[0].accession) + collection_objects_to_set = [self.collectionobjects[0], self.collectionobjects[3]] accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), 'collectionobjects': [ - api.obj_to_data(collection_object) for collection_object in collection_objects_to_set + api.obj_to_data(collection_object) if index % 2 == 0 + else api.uri_for_model('collectionobject', collection_object.id) + for index, collection_object in enumerate(collection_objects_to_set) ] } accession = api.update_obj(self.collection, self.agent, 'Accession', accession.id, accession.version, accession_data) @@ -553,6 +560,8 @@ def test_updating_independent_resource(self): ] } + self.assertEqual(self.collectionobjects[2].integer1, None) + self.assertEqual(list(self.collectionobjects[2].determinations.all()), []) accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.collectionobjects[2].refresh_from_db() self.assertEqual(self.collectionobjects[2].integer1, 10) @@ -582,17 +591,54 @@ def test_reassigning_independent(self): self.collectionobjects[0].accession = acc1 self.collectionobjects[0].save() + self.collectionobjects[1].accession = acc1 + self.collectionobjects[1].save() accession_data = { 'accessionnumber': "b", 'division': api.uri_for_model('division', self.division.id), 'collectionobjects': [ - api.obj_to_data(self.collectionobjects[0]) + api.obj_to_data(self.collectionobjects[0]), + api.uri_for_model('collectionobject', self.collectionobjects[1].id) ] } acc2 = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.collectionobjects[0].refresh_from_db() + self.collectionobjects[1].refresh_from_db() self.assertEqual(self.collectionobjects[0].accession, acc2) + self.assertEqual(self.collectionobjects[1].accession, acc2) + + def inline_error_handling(self): + collection_object_data = { + 'id': self.collectionobjects[0].id, + 'catalognumber': self.collectionobjects[0].catalognumber, + 'collection': api.uri_for_model('Collection', self.collection.id), + 'determinations': f'/api/specify/determination/?collectionobject={self.collectionobjects[0].id}' + } + + with self.assertRaises(AssertionError): + api.update_obj(self.collection, self.agent, + 'Collectionobject', self.collectionobjects[0].id, + self.collectionobjects[0].version, collection_object_data) + + new_determination = models.Determination.objects.create( + collectionobject=self.collectionobjects[1] + ) + + other_collection_object_data = { + 'id': self.collectionobjects[0].id, + 'catalognumber': self.collectionobjects[0].catalognumber, + 'collection': api.uri_for_model('Collection', self.collection.id), + 'determinations': [ + api.uri_for_model('determination', new_determination.id) + ] + } + + with self.assertRaises(AssertionError): + api.update_obj(self.collection, self.agent, + 'Collectionobject', self.collectionobjects[0].id, + self.collectionobjects[0].version, other_collection_object_data) + # version control on inlined resources should be tested From 08544c3d38ee24107430a5ef38f7e68300a406ef Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 12:11:18 -0500 Subject: [PATCH 016/132] Allow creating new resources from independent to-one side --- .../lib/components/DataModel/resourceApi.ts | 77 ++++++++++++++++--- specifyweb/specify/api.py | 13 ++-- specifyweb/specify/tests/test_api.py | 49 ++++++++++-- 3 files changed, 115 insertions(+), 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 799639f91dc..c3d23b82b8a 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -300,10 +300,61 @@ export const ResourceBase = Backbone.Model.extend({ this.dependentResources[field.name.toLowerCase()] = toMany; toMany.on('all', eventHandlerForToMany(toMany, field), this); }, - storeIndependentToMany(field: Relationship, toMany: Collection) { - assert(!field.isDependent() && relationshipIsToMany(field)); - const oldToMany = this.independentResources[field.name.toLowerCase()]; - if (oldToMany !== undefined) oldToMany.off('all', null, this); + storeIndependent( + field: Relationship, + related: Collection | SpecifyResource | null + ) { + assert(!field.isDependent()); + + if (field.type === 'one-to-many') + this._storeIndependentToMany(field, related); + else this._storeIndependentToOne(field, related); + }, + _storeIndependentToOne( + field: Relationship, + related: SpecifyResource | null + ) { + const oldRelated = this.independentResources[field.name.toLowerCase()]; + if (!related) { + if (oldRelated) { + oldRelated.off('all', null, this); + this.trigger('saverequired'); + } + this.independentResources[field.name.toLowerCase()] = null; + return; + } + related.toApiJSON = (options) => + related.isNew() || related.needsSaved + ? related.toJSON(options) + : related.url(); + + if (oldRelated && oldRelated.cid === related.cid) return; + + oldRelated && oldRelated.off('all', null, this); + + related.on('all', eventHandlerForToOne(related, field), this); + + switch (field.type) { + case 'one-to-one': + case 'many-to-one': { + this.independentResources[field.name.toLowerCase()] = related; + break; + } + case 'zero-to-one': { + this.independentResources[field.name.toLowerCase()] = related; + related.set(field.otherSideName, this.url()); + break; + } + default: { + throw new Error( + `setDependentToOne: unhandled field type: ${field.type}` + ); + } + } + }, + _storeIndependentToMany(field: Relationship, toMany: Collection) { + const oldIndependent = this.independentResources[field.name.toLowerCase()]; + if (oldIndependent !== undefined) oldIndependent.off('all', null, this); this.independentResources[field.name.toLowerCase()] = toMany; toMany.on('all', eventHandlerForToMany(toMany, field), this); @@ -428,7 +479,7 @@ export const ResourceBase = Backbone.Model.extend({ }, _handleInlineDataOrResource(value, fieldName) { // BUG: check type of value - const field: Relationship = this.specifyTable.getField(fieldName); + const field: Relationship = this.specifyTable.strictGetField(fieldName); const relatedTable = field.relatedTable; // BUG: don't do anything for virtual fields @@ -448,7 +499,7 @@ export const ResourceBase = Backbone.Model.extend({ collectionOptions, value ); - this.storeIndependentToMany(field, collection); + this.storeIndependent(field, collection); } // Because the foreign key is on the other side @@ -469,13 +520,14 @@ export const ResourceBase = Backbone.Model.extend({ * BUG: tighten up this check. * The FK is null, or not a URI or inlined resource at any rate */ - field.isDependent() && this.storeDependent(field, null); + if (field.isDependent()) this.storeDependent(field, null); + else this.storeIndependent(field, null); return value; } const toOne = maybeMakeResource(value, relatedTable); - - field.isDependent() && this.storeDependent(field, toOne); + if (field.isDependent()) this.storeDependent(field, toOne); + else this.storeIndependent(field, toOne); this.trigger(`change:${fieldName}`, this); this.trigger('change', this); return toOne.url(); @@ -646,6 +698,8 @@ export const ResourceBase = Backbone.Model.extend({ if (field.isDependent()) { console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); + } else { + this.storeIndependent(field, toOne); } } // If we want a field within the related resource then recur @@ -762,7 +816,7 @@ export const ResourceBase = Backbone.Model.extend({ : existingToMany; return collection.fetch({ limit: 0 }).then((collection) => { - this.storeIndependentToMany(field, collection); + this.storeIndependent(field, collection); return collection; }); }, @@ -835,8 +889,7 @@ export const ResourceBase = Backbone.Model.extend({ Object.entries(self.independentResources).forEach( ([fieldName, related]) => { - const field = self.specifyTable.getField(fieldName); - if (field.type === 'one-to-many' && related) { + if (related) { json[fieldName] = related.toApiJSON(); } } diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 1fd30ce3c74..632a2fea366 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -600,20 +600,19 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List elif hasattr(val, 'items'): # i.e. it's a dict of some sort # The related object is represented by a nested dict of data. - assert dependent, "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) rel_model = field.related_model if 'id' in val: # The related object is an existing resource with an id. - # This should never happen. + # This should never happen for dependent resources. rel_obj = update_obj(collection, agent, rel_model, val['id'], val['version'], val, - parent_obj=obj) + parent_obj=obj if dependent else None) else: # The related object is to be created. rel_obj = create_obj(collection, agent, rel_model, val, - parent_obj=obj) + parent_obj=obj if dependent else None) setattr(obj, field_name, rel_obj) if dependent and old_related and old_related.id != rel_obj.id: @@ -678,11 +677,11 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: rel_obj = update_obj(collection, agent, rel_model, rel_data['id'], rel_data['version'], rel_data, - parent_obj=obj if is_dependent_field(obj, field_name) else None) + parent_obj=obj if is_dependent else None) else: # Create a new related object. - rel_obj = create_obj(collection, agent, rel_model, rel_data, parent_obj=obj) + rel_obj = create_obj(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) if not is_dependent and not (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): getattr(obj, field_name).add(rel_obj) @@ -807,7 +806,7 @@ def parse_uri(uri: str) -> Tuple[str, str]: def strict_uri_to_model(uri: str, model: str) -> Tuple[str, int]: uri_model, uri_id = parse_uri(uri) - assert model.lower() == uri_model.lower() + assert model.lower() == uri_model.lower(), f"{model} does not match model in uri: {uri_model}" assert uri_id is not None return uri_model, int(uri_id) diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index 1c184203fba..be6fa4a632c 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -494,7 +494,7 @@ def test_update_object_with_more_inlines(self): self.assertEqual(obj.collectionobjectattribute.text1, text1_data) - def test_independent_set_inline(self): + def test_independent_to_many_set_inline(self): accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), @@ -510,7 +510,19 @@ def test_independent_set_inline(self): self.assertEqual(accession, self.collectionobjects[0].accession) self.assertEqual(accession, self.collectionobjects[1].accession) - def test_indepenent_removing_from_inline(self): + def test_independent_to_one_set_inline(self): + collection_object_data = { + 'collection': api.uri_for_model('collection', self.collection.id), + 'accession': { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id), + } + } + + created_co = api.create_obj(self.collection, self.agent, 'Collectionobject', collection_object_data) + self.assertIsNotNone(created_co.accession) + + def test_indepenent_to_many_removing_from_inline(self): accession = models.Accession.objects.create( accessionnumber="a", version="0", @@ -539,7 +551,7 @@ def test_indepenent_removing_from_inline(self): # ensure the other CollectionObjects have not been deleted self.assertEqual(len(models.Collectionobject.objects.all()), len(self.collectionobjects)) - def test_updating_independent_resource(self): + def test_updating_independent_to_many_resource(self): co_to_modify = api.obj_to_data(self.collectionobjects[2]) co_to_modify.update({ 'integer1': 10, @@ -567,7 +579,34 @@ def test_updating_independent_resource(self): self.assertEqual(self.collectionobjects[2].integer1, 10) self.assertEqual(len(self.collectionobjects[2].determinations.all()), 1) - def test_independent_creating_from_remoteside(self): + def test_updating_independent_to_one_resource(self): + accession_data = { + 'accessionnumber': "a", + 'division': api.uri_for_model('division', self.division.id) + } + accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) + + accession_text = 'someText' + accession_data.update({ + 'id': accession.id, + 'accessionnumber': "a1", + 'text1': accession_text, + 'version': accession.version + }) + + collection_object_data = { + 'collection': api.uri_for_model('collection', self.collection.id), + 'accession': accession_data + } + + self.assertEqual(accession.text1, None) + self.assertEqual(accession.accessionnumber, 'a') + created_co = api.create_obj(self.collection, self.agent, 'Collectionobject', collection_object_data) + accession.refresh_from_db() + self.assertEqual(accession.text1, accession_text) + self.assertEqual(accession.accessionnumber, 'a1') + + def test_independent_to_many_creating_from_remoteside(self): new_catalognumber = f'num-{len(self.collectionobjects)}' accession_data = { 'accessionnumber': "a", @@ -583,7 +622,7 @@ def test_independent_creating_from_remoteside(self): accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.assertTrue(models.Collectionobject.objects.filter(catalognumber=new_catalognumber).exists()) - def test_reassigning_independent(self): + def test_reassigning_independent_to_many(self): acc1 = models.Accession.objects.create( accessionnumber="a", division = self.division From 70c33453f65692812d7b7f969eddf9cd0ad5b8ba Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 12:31:15 -0500 Subject: [PATCH 017/132] Remove toApiJSON method declaration on independent to-one resources --- .../js_src/lib/components/DataModel/resourceApi.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index c3d23b82b8a..bbce67c7e0a 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -323,10 +323,6 @@ export const ResourceBase = Backbone.Model.extend({ this.independentResources[field.name.toLowerCase()] = null; return; } - related.toApiJSON = (options) => - related.isNew() || related.needsSaved - ? related.toJSON(options) - : related.url(); if (oldRelated && oldRelated.cid === related.cid) return; @@ -890,7 +886,11 @@ export const ResourceBase = Backbone.Model.extend({ Object.entries(self.independentResources).forEach( ([fieldName, related]) => { if (related) { - json[fieldName] = related.toApiJSON(); + json[fieldName] = isRelationshipCollection(related) + ? related.toApiJSON() + : related.isNew() || related.needsSaved + ? related.toJSON(options) + : related.url(); } } ); From 02e262ec14ebd4b33c707a501715f9a2570ba787 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 19:53:52 -0500 Subject: [PATCH 018/132] Remove changedResources on IndependentCollection --- .../lib/components/DataModel/collectionApi.ts | 36 +++++-------------- .../js_src/lib/components/Forms/SubView.tsx | 2 +- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 34371d71228..82939ab4596 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -1,13 +1,12 @@ // @ts-nocheck import _ from 'underscore'; -import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { formatUrl } from '../Router/queryString'; import { Backbone } from './backbone'; -import { AnySchema } from './helperTypes'; -import { SpecifyResource } from './legacyTypes'; +import type { AnySchema } from './helperTypes'; +import type { SpecifyResource } from './legacyTypes'; // REFACTOR: remove @ts-nocheck @@ -103,25 +102,11 @@ export const IndependentCollection = Base.extend({ this.domainfilter = Boolean(options.domainfilter) && this.model?.specifyTable.getScopingRelationship() !== undefined; - - this.changedResources = Object.fromEntries( - records.map((record) => [record.cid, false]) - ); }, initialize(_tables, options) { this.on( - 'change', + 'change add remove', function (resource: SpecifyResource) { - if (!resource.isBeingInitialized()) - this.changedResources[resource.cid] = true; - this.trigger('saverequired'); - }, - this - ); - this.on( - 'add remove', - function (resource: SpecifyResource) { - this.changedResources[resource.cid] = false; this.trigger('saverequired'); }, this @@ -171,16 +156,11 @@ export const IndependentCollection = Base.extend({ }, toApiJSON(options) { const self = this; - const resources = - Object.keys(self.changedResources).length === 0 - ? formatUrl(this.url(), this.filters) - : this.map(function (resource) { - return self.changedResources[resource.cid] === true - ? resource.toJSON(options) - : resource.url(); - }); - this.changedResources = []; - return resources; + + return this.map(function (resource: SpecifyResource) { + const formatAsObject = resource.needsSaved || resource.isNew(); + return formatAsObject ? resource.toJSON(options) : resource.url(); + }); }, }); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 51f181452e2..ec62321d046 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -4,7 +4,7 @@ import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; -import { overwriteReadOnly, RA } from '../../utils/types'; +import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { attachmentSettingsPromise } from '../Attachments/attachments'; From a2d3574d3d9fd0bee7cc417c156c1b85f066bf82 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 20:17:08 -0500 Subject: [PATCH 019/132] Display loading indicator while fetching collection --- .../frontend/js_src/lib/components/Forms/SubView.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index ec62321d046..43f526d3a52 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -7,6 +7,7 @@ import { commonText } from '../../localization/common'; import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; +import { DataEntry } from '../Atoms/DataEntry'; import { attachmentSettingsPromise } from '../Attachments/attachments'; import { attachmentRelatedTables } from '../Attachments/utils'; import { ReadOnlyContext } from '../Core/Contexts'; @@ -265,7 +266,16 @@ export function SubView({ } />
- ) : undefined} + ) : isAttachmentMisconfigured ? undefined : ( + + + + {relationship.label} + + + {commonText.loading()} + + )} ); } From cf89d5be54694930fc07f778b7380d55f0b4d6fe Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 21:09:17 -0500 Subject: [PATCH 020/132] Use ResourceView dialog when adding independent resource --- .../FormSliders/IntegratedRecordSelector.tsx | 41 ++++++++++++++++++- .../lib/components/QueryComboBox/index.tsx | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 120c896eba6..b2bf7fc3b5b 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { State } from 'typesafe-reducer'; import { useSearchParameter } from '../../hooks/navigation'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -71,6 +72,14 @@ export function IntegratedRecordSelector({ const [isCollapsed, _handleCollapsed, handleExpand, handleToggle] = useBooleanState(defaultCollapsed); + const [state, setState] = React.useState< + | State< + 'AddResourceState', + { readonly resource: SpecifyResource } + > + | State<'MainState'> + >({ type: 'MainState' }); + const blockers = useAllSaveBlockers(collection.related, relationship); const hasBlockers = blockers.length > 0; React.useEffect(() => { @@ -203,10 +212,22 @@ export function IntegratedRecordSelector({ (isToOne && collection.models.length > 0) } onClick={(): void => { - focusFirstField(); const resource = new collection.table.specifyTable.Resource(); - handleAdd([resource]); + + if (isDependent) { + focusFirstField(); + handleAdd([resource]); + return; + } + + if (state.type === 'AddResourceState') + setState({ type: 'MainState' }); + else + setState({ + type: 'AddResourceState', + resource, + }); }} /> ) : undefined} @@ -279,6 +300,22 @@ export function IntegratedRecordSelector({ /> ) : null} {dialogs} + {state.type === 'AddResourceState' && + typeof handleAdd === 'function' ? ( + setState({ type: 'MainState' })} + onDeleted={undefined} + onSaved={(): void => { + handleAdd([state.resource]); + setState({ type: 'MainState' }); + }} + /> + ) : null} )} diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx index 98605a30a2c..da1e7ed286c 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx @@ -508,7 +508,7 @@ export function QueryComboBox({ } /> )} - {hasViewButton && hasTablePermission(relatedTable.name, 'create') + {hasViewButton && hasTablePermission(relatedTable.name, 'read') ? viewButton : undefined} From ad5a8eff58f14fdab29f32ac1cf95efe4a7e9e17 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 21:18:22 -0500 Subject: [PATCH 021/132] Only show loading indicators for non-button subviews --- specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 43f526d3a52..55942d9c4c1 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -266,7 +266,7 @@ export function SubView({ } /> - ) : isAttachmentMisconfigured ? undefined : ( + ) : isButton ? undefined : ( From d555321a0627ce943f20c572e1b69c622fae4b90 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 21:22:51 -0500 Subject: [PATCH 022/132] Only show ResourceView dialog when specified viewname differs from table view --- .../lib/components/FormSliders/IntegratedRecordSelector.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index b2bf7fc3b5b..2d81ddd1c16 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -215,7 +215,10 @@ export function IntegratedRecordSelector({ const resource = new collection.table.specifyTable.Resource(); - if (isDependent) { + if ( + isDependent || + viewName === relationship.relatedTable.view + ) { focusFirstField(); handleAdd([resource]); return; From 2664cf1384d6d8ab5151f26a868bb4a86bb45e80 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 21:41:51 -0500 Subject: [PATCH 023/132] Use aria-pressed on add button when ResourceView is open --- .../lib/components/FormSliders/IntegratedRecordSelector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 2d81ddd1c16..66d6037ff93 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -207,6 +207,7 @@ export function IntegratedRecordSelector({ 'create' ) && typeof handleAdd === 'function' ? ( 0) From 14e2ab3b1bf017bf3dcb2c15bd39ad285ca56380 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 22 Aug 2024 22:21:42 -0500 Subject: [PATCH 024/132] Refactor getDependentToMany --- .../lib/components/DataModel/resourceApi.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index bbce67c7e0a..3ebfe5aa75f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -755,40 +755,40 @@ export const ResourceBase = Backbone.Model.extend({ ): Promise> { assert(field.isDependent()); + const self = this; const fieldName = field.name.toLowerCase(); const relatedTable = field.relatedTable; - let toMany = this.dependentResources[fieldName]; - - if (toMany) return toMany; + const existingToMany: Collection | undefined = + this.dependentResources[fieldName]; const collectionOptions = { field: field.getReverse(), related: this, }; - if (this.isNew()) { - toMany = new relatedTable.DependentCollection(collectionOptions, []); - this.storeDependent(field, toMany); - return toMany; - } else { + if (!this.isNew() && existingToMany === undefined) console.warn('expected dependent resource to be in cache'); - const temporaryCollection = new relatedTable.ToOneCollection( - collectionOptions - ); - return temporaryCollection - .fetch({ limit: 0 }) - .then( - () => - new relatedTable.DependentCollection( - collectionOptions, - temporaryCollection.tables - ) - ) - .then((toMany) => { - _this.storeDependent(field, toMany); - }); - } + + const collection = + existingToMany !== undefined + ? existingToMany + : this.isNew() + ? new relatedTable.DependentCollection(collectionOptions, []) + : await new relatedTable.ToOneCollection(collectionOptions) + .fetch({ limit: 0 }) + .then( + (collection) => + new relatedTable.DependentCollection( + collectionOptions, + collection.models + ) + ); + + return collection.fetch({ limit: 0 }).then((collection) => { + self.storeDependent(field, collection); + return collection; + }); }, async getIndependentToMany( field: Relationship From d47585db14762d48cff850ea103319da3e1617e2 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 27 Aug 2024 13:40:07 -0500 Subject: [PATCH 025/132] Fix bug when fetching independent collection when related is initializing --- .../lib/components/DataModel/collectionApi.ts | 8 ++++---- .../lib/components/DataModel/resourceApi.ts | 18 +++++++++--------- specifyweb/specify/api.py | 19 ++++++++++--------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 82939ab4596..556161ca286 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -128,7 +128,7 @@ export const IndependentCollection = Base.extend({ return records; }, async fetch(options) { - if (this.related.isNew()) { + if (this.related.isNew() || this.related.isBeingInitialized()) { return this; } @@ -144,11 +144,11 @@ export const IndependentCollection = Base.extend({ domainfilter: this.domainfilter, limit: options.limit, }; - + const self = this; this._fetch = Backbone.Collection.prototype.fetch.call(this, options); return this._fetch.then(() => { - this._fetch = null; - return this; + self._fetch = null; + return self; }); }, isComplete() { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 3ebfe5aa75f..2219ab516db 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -7,7 +7,6 @@ import { Http } from '../../utils/ajax/definitions'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; import { isRelationshipCollection } from './collectionApi'; @@ -112,7 +111,7 @@ export const ResourceBase = Backbone.Model.extend({ _save: null, // Stores reference to the ajax deferred while the resource is being saved /** - * Returns true if the resource is being fetched and saved from Backbone + * Returns true if the resource is being fetched or saved from Backbone * More specifically, returns true while this resource holds a reference * to Backbone's save() and fetch() in _save and _fetch */ @@ -811,9 +810,9 @@ export const ResourceBase = Backbone.Model.extend({ ? new relatedTable.IndependentCollection(collectionOptions) : existingToMany; - return collection.fetch({ limit: 0 }).then((collection) => { - this.storeIndependent(field, collection); - return collection; + return collection.fetch({ limit: 0 }).then((fetchedCollection) => { + this.storeIndependent(field, fetchedCollection); + return fetchedCollection; }); }, async save({ @@ -872,14 +871,15 @@ export const ResourceBase = Backbone.Model.extend({ }, toJSON() { const self = this; - const json = Backbone.Model.prototype.toJSON.apply(self, arguments); + const options = arguments; + const json = Backbone.Model.prototype.toJSON.apply(self, options); _.each(self.dependentResources, (related, fieldName) => { const field = self.specifyTable.getField(fieldName); if (field.type === 'zero-to-one') { - json[fieldName] = related ? [related.toJSON()] : []; + json[fieldName] = related ? [related.toJSON(options)] : []; } else { - json[fieldName] = related ? related.toJSON() : null; + json[fieldName] = related ? related.toJSON(options) : null; } }); @@ -887,7 +887,7 @@ export const ResourceBase = Backbone.Model.extend({ ([fieldName, related]) => { if (related) { json[fieldName] = isRelationshipCollection(related) - ? related.toApiJSON() + ? related.toApiJSON(options) : related.isNew() || related.needsSaved ? related.toJSON(options) : related.url(); diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 632a2fea366..2c90c15fc03 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -672,16 +672,8 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: #FIXME: only update related objects when they change rel_data[field.field.name] = obj - if 'id' in rel_data: - # Update an existing related object. - rel_obj = update_obj(collection, agent, - rel_model, rel_data['id'], - rel_data['version'], rel_data, - parent_obj=obj if is_dependent else None) - else: - # Create a new related object. - rel_obj = create_obj(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) + rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) if not is_dependent and not (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): getattr(obj, field_name).add(rel_obj) @@ -699,6 +691,15 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: else: getattr(obj, field_name).remove(*list(to_remove)) +def update_or_create_resource(collection, agent, model, data, parent_obj): + if 'id' in data: + return update_obj(collection, agent, + model, data['id'], + data['version'], data, + parent_obj=parent_obj) + else: + return create_obj(collection, agent, model, data, parent_obj=parent_obj) + @transaction.atomic def delete_resource(collection, agent, name, id, version) -> None: """Delete the resource with 'id' and model named 'name' with optimistic From 5c9f22d40c4ae9172ee48639c07a8aaa7d50eac8 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 27 Aug 2024 15:31:16 -0500 Subject: [PATCH 026/132] Only update related version when changed --- specifyweb/specify/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 2c90c15fc03..59e9a37f00d 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -669,8 +669,10 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: assert not is_dependent, "expected object for dependent field %s in %s: %s" % (field_name, obj, rel_data) fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) rel_data = cached_objs[fk_id] + if rel_data[field.field.name] == uri_for_model(obj.__class__, obj.id): + ids.append(rel_data["id"]) + continue - #FIXME: only update related objects when they change rel_data[field.field.name] = obj rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) From 3a6b9b5a7419cdf40e442e30459499d33c89a566 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 28 Aug 2024 22:11:13 -0500 Subject: [PATCH 027/132] Fix test not being automatically ran --- specifyweb/specify/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index be6fa4a632c..2646bbd1d66 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -647,7 +647,7 @@ def test_reassigning_independent_to_many(self): self.assertEqual(self.collectionobjects[0].accession, acc2) self.assertEqual(self.collectionobjects[1].accession, acc2) - def inline_error_handling(self): + def test_inline_error_handling(self): collection_object_data = { 'id': self.collectionobjects[0].id, 'catalognumber': self.collectionobjects[0].catalognumber, From 2e46526f4a8dbac57d9f8ca2682bfe34174d6be4 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Sep 2024 09:36:40 -0500 Subject: [PATCH 028/132] Show helpful error when rendering to-many as querycbx --- .../lib/components/FormEditor/viewSpec.ts | 30 +++++++++++++++++-- .../js_src/lib/components/FormParse/fields.ts | 10 +++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts index 55d488e32af..772e53d6d3d 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts @@ -17,6 +17,7 @@ import { syncers } from '../Syncer/syncers'; import type { SimpleXmlNode } from '../Syncer/xmlToJson'; import { createSimpleXmlNode } from '../Syncer/xmlToJson'; import { createXmlSpec } from '../Syncer/xmlUtils'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; /* eslint-disable @typescript-eslint/no-magic-numbers */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -799,8 +800,32 @@ const textAreaSpec = ( ), }); -const queryComboBoxSpec = f.store(() => +const queryComboBoxSpec = ( + _spec: SpecToJson>, + { + table, + }: { + readonly table: SpecifyTable | undefined; + } +) => createXmlSpec({ + field: pipe( + rawFieldSpec(table).field, + syncer( + ({ parsed, ...rest }) => { + if ( + parsed?.some( + (field) => field.isRelationship && relationshipIsToMany(field) + ) + ) + console.error( + 'Unable to render a to-many relationship as a querycbx. Use a Subview instead' + ); + return { parsed, ...rest }; + }, + (value) => value + ) + ), // Customize view name dialogViewName: syncers.xmlAttribute('initialize displayDlg', 'skip'), searchDialogViewName: syncers.xmlAttribute('initialize searchDlg', 'skip'), @@ -836,8 +861,7 @@ const queryComboBoxSpec = f.store(() => syncers.maybe(syncers.toBoolean), syncers.default(true) ), - }) -); + }); const checkBoxSpec = f.store(() => createXmlSpec({ diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts index 1f06902f7f8..cc723f79eda 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts @@ -24,6 +24,7 @@ import { getBooleanAttribute, getParsedAttribute, } from '../Syncer/xmlUtils'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { PluginDefinition } from './plugins'; import { parseUiPlugin } from './plugins'; @@ -222,6 +223,15 @@ const processFieldType: { if (fields === undefined) { console.error('Trying to render a query combobox without a field name'); return { type: 'Blank' }; + } else if ( + fields.some( + (field) => field.isRelationship && relationshipIsToMany(field) + ) + ) { + console.error( + 'Unable to render a to-many relationship as a querycbx. Use a Subview instead' + ); + return { type: 'Blank' }; } else if (fields.at(-1)?.isRelationship === true) { return { type: 'QueryComboBox', From 5921a397bb85a0b45c918f6e52cdae43eac4c0b4 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Sep 2024 10:21:41 -0500 Subject: [PATCH 029/132] Hide relationship when needed to avoid cyclical rendering --- .../js_src/lib/components/Forms/SubView.tsx | 170 ++++++++++-------- 1 file changed, 100 insertions(+), 70 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 55942d9c4c1..d8356bf81d1 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -4,6 +4,7 @@ import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; +import type { RA } from '../../utils/types'; import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; @@ -23,18 +24,26 @@ import { IntegratedRecordSelector } from '../FormSliders/IntegratedRecordSelecto import { TableIcon } from '../Molecules/TableIcon'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -export const SubViewContext = React.createContext< +type SubViewContextType = | { readonly relationship: Relationship | undefined; readonly formType: FormType; readonly sortField: SubViewSortField | undefined; + /** + * Don't render a relationship if it is already being rendered in a + * parent subview. + * Avoids infinite cycles in rendering forms + */ + readonly parentContext: RA | undefined; readonly handleChangeFormType: (formType: FormType) => void; readonly handleChangeSortField: ( sortField: SubViewSortField | undefined ) => void; } - | undefined ->(undefined); + | undefined; + +export const SubViewContext = + React.createContext(undefined); SubViewContext.displayName = 'SubViewContext'; export function SubView({ @@ -175,17 +184,31 @@ export function SubView({ ), [parentResource, relationship, fetchCollection] ); + const subviewContext = React.useContext(SubViewContext); const [formType, setFormType] = useTriggerState(initialFormType); - const contextValue = React.useMemo( + const parentContext = React.useMemo( + () => subviewContext?.parentContext ?? [], + [subviewContext?.parentContext] + ); + + const contextValue = React.useMemo( () => ({ relationship, formType, sortField, + parentContext: [...parentContext, relationship], handleChangeFormType: setFormType, handleChangeSortField: setSortField, }), - [relationship, formType, sortField, setFormType, setSortField] + [ + relationship, + formType, + sortField, + parentContext, + setFormType, + setSortField, + ] ); const isReadOnly = React.useContext(ReadOnlyContext); @@ -203,11 +226,13 @@ export function SubView({ return ( - {isButton && ( - + {isButton && ( + 0 @@ -215,66 +240,71 @@ export function SubView({ : '' } ${isOpen ? '!bg-brand-300 dark:!bg-brand-500' : ''}`} - title={relationship.label} - onClick={handleToggle} - > - { - /* - * Attachment table icons have lots of vertical white space, making - * them look overly small on the forms. - * See https://github.com/specify/specify7/issues/1259 - * Thus, have to introduce some inconsistency here - */ - parentFormType === 'form' && ( - - ) - } - - {collection?.models.length ?? commonText.loading()} - - - )} - {typeof collection === 'object' && isOpen ? ( - - - void parentResource.set( - relationship.name, - resource as never - ) - } - onClose={handleClose} - onDelete={ - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ? undefined - : (): void => - void parentResource.set(relationship.name, null as never) - } - /> - - ) : isButton ? undefined : ( - - - - {relationship.label} - - - {commonText.loading()} - + title={relationship.label} + onClick={handleToggle} + > + { + /* + * Attachment table icons have lots of vertical white space, making + * them look overly small on the forms. + * See https://github.com/specify/specify7/issues/1259 + * Thus, have to introduce some inconsistency here + */ + parentFormType === 'form' && ( + + ) + } + + {collection?.models.length ?? commonText.loading()} + + + )} + {typeof collection === 'object' && isOpen ? ( + + + void parentResource.set( + relationship.name, + resource as never + ) + } + onClose={handleClose} + onDelete={ + relationshipIsToMany(relationship) && + relationship.type !== 'zero-to-one' + ? undefined + : (): void => + void parentResource.set( + relationship.name, + null as never + ) + } + /> + + ) : isButton ? undefined : ( + + + + {relationship.label} + + + {commonText.loading()} + + )} + )} ); From 137a2779904d407a4fe094c5710f631099f9c405 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Sep 2024 10:49:28 -0500 Subject: [PATCH 030/132] Resolve type errors --- .../js_src/lib/components/DataModel/resourceApi.ts | 10 +++------- .../js_src/lib/components/DataModel/saveBlockers.tsx | 4 ++-- .../js_src/lib/components/DataModel/specifyTable.ts | 2 +- .../js_src/lib/components/FormCells/FormTable.tsx | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 2219ab516db..45a39644a57 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -87,16 +87,12 @@ function eventHandlerForToMany(_related, field) { // Always returns a resource const maybeMakeResource = < - TABLE extends SpecifyTable, + TABLE extends SpecifyTable, TABLE_SCHEMA extends Tables[TABLE['name']] >( value: - | SpecifyResource - | Partial< - | SerializedResource - | SerializedRecord - | { readonly id?: number } - >, + | Partial | SerializedResource> + | SpecifyResource, relatedTable: TABLE ): SpecifyResource => value instanceof ResourceBase diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx index 28501c12dc2..ed6ce30ac50 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx @@ -109,8 +109,8 @@ export function useSaveBlockers( ]; } -export function setSaveBlockers( - resource: SpecifyResource, +export function setSaveBlockers( + resource: SpecifyResource, field: LiteralField | Relationship, errors: Parameters>[typeof SET]>[0], blockerKey: string diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index b8dc99894ef..43bcfdd833d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -74,7 +74,7 @@ type CollectionConstructor = new ( >; readonly domainfilter?: boolean; }, - initalResources?: RA> + initalResources?: RA> ) => UnFetchedCollection; export type UnFetchedCollection = { diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index acc11b08d04..d6173ee3495 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -30,7 +30,7 @@ import type { SortConfig } from '../Molecules/Sorting'; import { SortIndicator } from '../Molecules/Sorting'; import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; -import { SearchDialog, useSearchDialog } from '../SearchDialog'; +import { useSearchDialog } from '../SearchDialog'; import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { FormCell } from './index'; From 014f87169d4dcdda4a57d533d0ffe355f41233c1 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Sep 2024 11:14:42 -0500 Subject: [PATCH 031/132] Resolve frontend tests --- .../__snapshots__/specifyTable.test.ts.snap | 104 ++++++++++++++++++ .../DataModel/__tests__/resource.test.ts | 2 + .../DataModel/__tests__/resourceApi.test.ts | 2 +- .../DataModel/__tests__/specifyTable.test.ts | 101 +---------------- .../Formatters/__tests__/formatters.test.ts | 2 +- .../components/Syncer/__tests__/index.test.ts | 4 +- .../tests/ajax/static/api/specify_trees.json | 2 - 7 files changed, 111 insertions(+), 106 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap index 38b0269a18c..a45d476e10c 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap @@ -26,6 +26,7 @@ exports[`fields are loaded 1`] = ` "[literalField CollectionObject.text2]", "[literalField CollectionObject.inventoryDate]", "[literalField CollectionObject.inventoryDatePrecision]", + "[literalField CollectionObject.isMemberOfCOG]", "[literalField CollectionObject.modifier]", "[literalField CollectionObject.name]", "[literalField CollectionObject.notifications]", @@ -101,6 +102,108 @@ exports[`fields are loaded 1`] = ` ] `; +exports[`indexed fields are loaded 1`] = ` +{ + "accession": "[relationship CollectionObject.accession]", + "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", + "agent1": "[relationship CollectionObject.agent1]", + "altCatalogNumber": "[literalField CollectionObject.altCatalogNumber]", + "appraisal": "[relationship CollectionObject.appraisal]", + "availability": "[literalField CollectionObject.availability]", + "catalogNumber": "[literalField CollectionObject.catalogNumber]", + "catalogedDate": "[literalField CollectionObject.catalogedDate]", + "catalogedDatePrecision": "[literalField CollectionObject.catalogedDatePrecision]", + "catalogedDateVerbatim": "[literalField CollectionObject.catalogedDateVerbatim]", + "cataloger": "[relationship CollectionObject.cataloger]", + "cojo": "[relationship CollectionObject.cojo]", + "collectingEvent": "[relationship CollectionObject.collectingEvent]", + "collection": "[relationship CollectionObject.collection]", + "collectionMemberId": "[literalField CollectionObject.collectionMemberId]", + "collectionObjectAttachments": "[relationship CollectionObject.collectionObjectAttachments]", + "collectionObjectAttribute": "[relationship CollectionObject.collectionObjectAttribute]", + "collectionObjectAttrs": "[relationship CollectionObject.collectionObjectAttrs]", + "collectionObjectCitations": "[relationship CollectionObject.collectionObjectCitations]", + "collectionObjectProperties": "[relationship CollectionObject.collectionObjectProperties]", + "collectionObjectType": "[relationship CollectionObject.collectionObjectType]", + "conservDescriptions": "[relationship CollectionObject.conservDescriptions]", + "container": "[relationship CollectionObject.container]", + "containerOwner": "[relationship CollectionObject.containerOwner]", + "countAmt": "[literalField CollectionObject.countAmt]", + "createdByAgent": "[relationship CollectionObject.createdByAgent]", + "currentDetermination": "[relationship CollectionObject.currentDetermination]", + "date1": "[literalField CollectionObject.date1]", + "date1Precision": "[literalField CollectionObject.date1Precision]", + "deaccessioned": "[literalField CollectionObject.deaccessioned]", + "description": "[literalField CollectionObject.description]", + "determinations": "[relationship CollectionObject.determinations]", + "dnaSequences": "[relationship CollectionObject.dnaSequences]", + "embargoAuthority": "[relationship CollectionObject.embargoAuthority]", + "embargoReason": "[literalField CollectionObject.embargoReason]", + "embargoReleaseDate": "[literalField CollectionObject.embargoReleaseDate]", + "embargoReleaseDatePrecision": "[literalField CollectionObject.embargoReleaseDatePrecision]", + "embargoStartDate": "[literalField CollectionObject.embargoStartDate]", + "embargoStartDatePrecision": "[literalField CollectionObject.embargoStartDatePrecision]", + "exsiccataItems": "[relationship CollectionObject.exsiccataItems]", + "fieldNotebookPage": "[relationship CollectionObject.fieldNotebookPage]", + "fieldNumber": "[literalField CollectionObject.fieldNumber]", + "guid": "[literalField CollectionObject.guid]", + "integer1": "[literalField CollectionObject.integer1]", + "integer2": "[literalField CollectionObject.integer2]", + "inventorizedBy": "[relationship CollectionObject.inventorizedBy]", + "inventoryDate": "[literalField CollectionObject.inventoryDate]", + "inventoryDatePrecision": "[literalField CollectionObject.inventoryDatePrecision]", + "isMemberOfCOG": "[literalField CollectionObject.isMemberOfCOG]", + "leftSideRels": "[relationship CollectionObject.leftSideRels]", + "modifiedByAgent": "[relationship CollectionObject.modifiedByAgent]", + "modifier": "[literalField CollectionObject.modifier]", + "name": "[literalField CollectionObject.name]", + "notifications": "[literalField CollectionObject.notifications]", + "number1": "[literalField CollectionObject.number1]", + "number2": "[literalField CollectionObject.number2]", + "numberOfDuplicates": "[literalField CollectionObject.numberOfDuplicates]", + "objectCondition": "[literalField CollectionObject.objectCondition]", + "ocr": "[literalField CollectionObject.ocr]", + "otherIdentifiers": "[relationship CollectionObject.otherIdentifiers]", + "paleoContext": "[relationship CollectionObject.paleoContext]", + "preparations": "[relationship CollectionObject.preparations]", + "projectNumber": "[literalField CollectionObject.projectNumber]", + "projects": "[relationship CollectionObject.projects]", + "remarks": "[literalField CollectionObject.remarks]", + "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", + "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", + "reservedText": "[literalField CollectionObject.reservedText]", + "reservedText2": "[literalField CollectionObject.reservedText2]", + "reservedText3": "[literalField CollectionObject.reservedText3]", + "restrictions": "[literalField CollectionObject.restrictions]", + "rightSideRels": "[relationship CollectionObject.rightSideRels]", + "sgrStatus": "[literalField CollectionObject.sgrStatus]", + "text1": "[literalField CollectionObject.text1]", + "text2": "[literalField CollectionObject.text2]", + "text3": "[literalField CollectionObject.text3]", + "text4": "[literalField CollectionObject.text4]", + "text5": "[literalField CollectionObject.text5]", + "text6": "[literalField CollectionObject.text6]", + "text7": "[literalField CollectionObject.text7]", + "text8": "[literalField CollectionObject.text8]", + "timestampCreated": "[literalField CollectionObject.timestampCreated]", + "timestampModified": "[literalField CollectionObject.timestampModified]", + "totalCountAmt": "[literalField CollectionObject.totalCountAmt]", + "totalValue": "[literalField CollectionObject.totalValue]", + "treatmentEvents": "[relationship CollectionObject.treatmentEvents]", + "uniqueIdentifier": "[literalField CollectionObject.uniqueIdentifier]", + "version": "[literalField CollectionObject.version]", + "visibility": "[literalField CollectionObject.visibility]", + "visibilitySetBy": "[relationship CollectionObject.visibilitySetBy]", + "voucherRelationships": "[relationship CollectionObject.voucherRelationships]", + "yesNo1": "[literalField CollectionObject.yesNo1]", + "yesNo2": "[literalField CollectionObject.yesNo2]", + "yesNo3": "[literalField CollectionObject.yesNo3]", + "yesNo4": "[literalField CollectionObject.yesNo4]", + "yesNo5": "[literalField CollectionObject.yesNo5]", + "yesNo6": "[literalField CollectionObject.yesNo6]", +} +`; + exports[`literal fields are loaded 1`] = ` [ "[literalField CollectionObject.actualTotalCountAmt]", @@ -127,6 +230,7 @@ exports[`literal fields are loaded 1`] = ` "[literalField CollectionObject.text2]", "[literalField CollectionObject.inventoryDate]", "[literalField CollectionObject.inventoryDatePrecision]", + "[literalField CollectionObject.isMemberOfCOG]", "[literalField CollectionObject.modifier]", "[literalField CollectionObject.name]", "[literalField CollectionObject.notifications]", diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts index beec6f4d451..7f4de6b9bad 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts @@ -294,6 +294,7 @@ test('getFieldsToNotClone', () => { 'catalogNumber', 'timestampModified', 'guid', + 'isMemberOfCOG', 'timestampCreated', 'totalCountAmt', 'uniqueIdentifier', @@ -307,6 +308,7 @@ test('getFieldsToNotClone', () => { 'catalogNumber', 'timestampModified', 'guid', + 'isMemberOfCOG', 'text1', 'timestampCreated', 'totalCountAmt', diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 57c4118a250..62f09a5fac7 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -147,7 +147,7 @@ const accessionsResponse = [ ]; overrideAjax( - '/api/specify/accession/?domainfilter=false&addressofrecord=42&offset=0', + '/api/specify/accession/?addressofrecord=42&domainfilter=false&limit=0', { meta: { total_count: 2 }, objects: accessionsResponse, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts index 8da5ba64de8..07b923db338 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts @@ -364,103 +364,4 @@ test('tableScoping', () => ).toMatchSnapshot()); test('indexed fields are loaded', () => - expect(tables.CollectionObject.field).toMatchInlineSnapshot(` - { - "accession": "[relationship CollectionObject.accession]", - "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", - "agent1": "[relationship CollectionObject.agent1]", - "altCatalogNumber": "[literalField CollectionObject.altCatalogNumber]", - "appraisal": "[relationship CollectionObject.appraisal]", - "availability": "[literalField CollectionObject.availability]", - "catalogNumber": "[literalField CollectionObject.catalogNumber]", - "catalogedDate": "[literalField CollectionObject.catalogedDate]", - "catalogedDatePrecision": "[literalField CollectionObject.catalogedDatePrecision]", - "catalogedDateVerbatim": "[literalField CollectionObject.catalogedDateVerbatim]", - "cataloger": "[relationship CollectionObject.cataloger]", - "cojo": "[relationship CollectionObject.cojo]", - "collectingEvent": "[relationship CollectionObject.collectingEvent]", - "collection": "[relationship CollectionObject.collection]", - "collectionMemberId": "[literalField CollectionObject.collectionMemberId]", - "collectionObjectAttachments": "[relationship CollectionObject.collectionObjectAttachments]", - "collectionObjectAttribute": "[relationship CollectionObject.collectionObjectAttribute]", - "collectionObjectAttrs": "[relationship CollectionObject.collectionObjectAttrs]", - "collectionObjectCitations": "[relationship CollectionObject.collectionObjectCitations]", - "collectionObjectProperties": "[relationship CollectionObject.collectionObjectProperties]", - "collectionObjectType": "[relationship CollectionObject.collectionObjectType]", - "conservDescriptions": "[relationship CollectionObject.conservDescriptions]", - "container": "[relationship CollectionObject.container]", - "containerOwner": "[relationship CollectionObject.containerOwner]", - "countAmt": "[literalField CollectionObject.countAmt]", - "createdByAgent": "[relationship CollectionObject.createdByAgent]", - "currentDetermination": "[relationship CollectionObject.currentDetermination]", - "date1": "[literalField CollectionObject.date1]", - "date1Precision": "[literalField CollectionObject.date1Precision]", - "deaccessioned": "[literalField CollectionObject.deaccessioned]", - "description": "[literalField CollectionObject.description]", - "determinations": "[relationship CollectionObject.determinations]", - "dnaSequences": "[relationship CollectionObject.dnaSequences]", - "embargoAuthority": "[relationship CollectionObject.embargoAuthority]", - "embargoReason": "[literalField CollectionObject.embargoReason]", - "embargoReleaseDate": "[literalField CollectionObject.embargoReleaseDate]", - "embargoReleaseDatePrecision": "[literalField CollectionObject.embargoReleaseDatePrecision]", - "embargoStartDate": "[literalField CollectionObject.embargoStartDate]", - "embargoStartDatePrecision": "[literalField CollectionObject.embargoStartDatePrecision]", - "exsiccataItems": "[relationship CollectionObject.exsiccataItems]", - "fieldNotebookPage": "[relationship CollectionObject.fieldNotebookPage]", - "fieldNumber": "[literalField CollectionObject.fieldNumber]", - "guid": "[literalField CollectionObject.guid]", - "integer1": "[literalField CollectionObject.integer1]", - "integer2": "[literalField CollectionObject.integer2]", - "inventorizedBy": "[relationship CollectionObject.inventorizedBy]", - "inventoryDate": "[literalField CollectionObject.inventoryDate]", - "inventoryDatePrecision": "[literalField CollectionObject.inventoryDatePrecision]", - "leftSideRels": "[relationship CollectionObject.leftSideRels]", - "modifiedByAgent": "[relationship CollectionObject.modifiedByAgent]", - "modifier": "[literalField CollectionObject.modifier]", - "name": "[literalField CollectionObject.name]", - "notifications": "[literalField CollectionObject.notifications]", - "number1": "[literalField CollectionObject.number1]", - "number2": "[literalField CollectionObject.number2]", - "numberOfDuplicates": "[literalField CollectionObject.numberOfDuplicates]", - "objectCondition": "[literalField CollectionObject.objectCondition]", - "ocr": "[literalField CollectionObject.ocr]", - "otherIdentifiers": "[relationship CollectionObject.otherIdentifiers]", - "paleoContext": "[relationship CollectionObject.paleoContext]", - "preparations": "[relationship CollectionObject.preparations]", - "projectNumber": "[literalField CollectionObject.projectNumber]", - "projects": "[relationship CollectionObject.projects]", - "remarks": "[literalField CollectionObject.remarks]", - "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", - "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", - "reservedText": "[literalField CollectionObject.reservedText]", - "reservedText2": "[literalField CollectionObject.reservedText2]", - "reservedText3": "[literalField CollectionObject.reservedText3]", - "restrictions": "[literalField CollectionObject.restrictions]", - "rightSideRels": "[relationship CollectionObject.rightSideRels]", - "sgrStatus": "[literalField CollectionObject.sgrStatus]", - "text1": "[literalField CollectionObject.text1]", - "text2": "[literalField CollectionObject.text2]", - "text3": "[literalField CollectionObject.text3]", - "text4": "[literalField CollectionObject.text4]", - "text5": "[literalField CollectionObject.text5]", - "text6": "[literalField CollectionObject.text6]", - "text7": "[literalField CollectionObject.text7]", - "text8": "[literalField CollectionObject.text8]", - "timestampCreated": "[literalField CollectionObject.timestampCreated]", - "timestampModified": "[literalField CollectionObject.timestampModified]", - "totalCountAmt": "[literalField CollectionObject.totalCountAmt]", - "totalValue": "[literalField CollectionObject.totalValue]", - "treatmentEvents": "[relationship CollectionObject.treatmentEvents]", - "uniqueIdentifier": "[literalField CollectionObject.uniqueIdentifier]", - "version": "[literalField CollectionObject.version]", - "visibility": "[literalField CollectionObject.visibility]", - "visibilitySetBy": "[relationship CollectionObject.visibilitySetBy]", - "voucherRelationships": "[relationship CollectionObject.voucherRelationships]", - "yesNo1": "[literalField CollectionObject.yesNo1]", - "yesNo2": "[literalField CollectionObject.yesNo2]", - "yesNo3": "[literalField CollectionObject.yesNo3]", - "yesNo4": "[literalField CollectionObject.yesNo4]", - "yesNo5": "[literalField CollectionObject.yesNo5]", - "yesNo6": "[literalField CollectionObject.yesNo6]", - } - `)); + expect(tables.CollectionObject.field).toMatchSnapshot()); diff --git a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts index ee14ccd2fc3..9b2f8fbd7df 100644 --- a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts @@ -120,7 +120,7 @@ const taxonCitation = { referenceWork: getResourceApiUrl('ReferenceWork', referenceWorkId), }; overrideAjax( - '/api/specify/taxoncitation/?domainfilter=false&referencework=1&offset=0', + '/api/specify/taxoncitation/?referencework=1&domainfilter=false&limit=0', { meta: { total_count: 1, diff --git a/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts b/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts index 89b362c28c0..bb7abf22a9c 100644 --- a/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts @@ -94,8 +94,8 @@ test('Editing Data Object Formatter', () => { - - + + " `); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json index 52fcc38131d..4a255c74ace 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json @@ -538,7 +538,6 @@ "version": 8, "createdbyagent": null, "modifiedbyagent": "/api/specify/agent/1514/", - "institutions": "/api/specify/institution/?storagetreedef=1", "treeentries": "/api/specify/storage/?definition=1", "treedefitems": [ { @@ -736,7 +735,6 @@ "version": 3, "createdbyagent": null, "modifiedbyagent": "/api/specify/agent/1514/", - "disciplines": "/api/specify/discipline/?geographytreedef=1", "treeentries": "/api/specify/geography/?definition=1", "treedefitems": [ { From 629b92c38fcd5d1db3c78243b56bf29bf2eb1ee0 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Sep 2024 11:23:29 -0500 Subject: [PATCH 032/132] Check independent update permission when removing from relationship --- specifyweb/specify/api.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 59e9a37f00d..939fb9949c5 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -601,18 +601,8 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List elif hasattr(val, 'items'): # i.e. it's a dict of some sort # The related object is represented by a nested dict of data. rel_model = field.related_model - if 'id' in val: - # The related object is an existing resource with an id. - # This should never happen for dependent resources. - rel_obj = update_obj(collection, agent, - rel_model, val['id'], - val['version'], val, - parent_obj=obj if dependent else None) - else: - # The related object is to be created. - rel_obj = create_obj(collection, agent, - rel_model, val, - parent_obj=obj if dependent else None) + + rel_obj = update_or_create_resource(collection, agent, rel_model, val, obj if dependent else None) setattr(obj, field_name, rel_obj) if dependent and old_related and old_related.id != rel_obj.id: @@ -677,8 +667,6 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) - if not is_dependent and not (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): - getattr(obj, field_name).add(rel_obj) ids.append(rel_obj.id) # Record the id as one to keep. # Delete related objects not in the ids list. @@ -691,6 +679,8 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: to_remove.delete() else: + for rel_obj in to_remove: + check_table_permissions(collection, agent, to_remove[0], 'update') getattr(obj, field_name).remove(*list(to_remove)) def update_or_create_resource(collection, agent, model, data, parent_obj): From 6ab0e34301b61f39ecd11cd8341502a99aa90e98 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 3 Sep 2024 13:24:37 -0500 Subject: [PATCH 033/132] Don't render SubView if reverse relationship doesn't exist --- .../js_src/lib/components/FormEditor/viewSpec.ts | 8 ++++++++ .../frontend/js_src/lib/components/Forms/SubView.tsx | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts index 772e53d6d3d..9a4a6ac76e9 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts @@ -369,6 +369,14 @@ const subViewSpec = ( console.error('SubView can only be used to display a relationship'); return undefined; } + if (field !== undefined && field.getReverse() === undefined) { + console.error( + `No reverse relationship exists${ + relationshipIsToMany(field) ? '' : '. Use a querycbx instead' + }` + ); + return undefined; + } if (field?.type === 'many-to-many') { // ResourceApi does not support .rget() on a many-to-many console.warn('Many-to-many relationships are not supported'); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index d8356bf81d1..ff0a60eb9c6 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -70,8 +70,9 @@ export function SubView({ const [sortField, setSortField] = useTriggerState(initialSortField); const fetchCollection = React.useCallback( + // If false is returned, then the Subview should not be rendered async function fetchCollection(): Promise< - Collection | undefined + Collection | false | undefined > { if ( relationshipIsToMany(relationship) && @@ -120,7 +121,7 @@ export function SubView({ `reverse relationship does not exist` ) ); - return undefined; + return false; } const collection = ( relationship.isDependent() @@ -156,7 +157,7 @@ export function SubView({ ); const [collection, setCollection] = React.useState< - Collection | undefined + Collection | false | undefined >(undefined); const versionRef = React.useRef(0); React.useEffect( @@ -226,7 +227,8 @@ export function SubView({ return ( - {parentContext.includes(relationship) ? undefined : ( + {parentContext.includes(relationship) || + collection === false ? undefined : ( <> {isButton && ( Date: Wed, 4 Sep 2024 10:21:31 -0500 Subject: [PATCH 034/132] Insert removal of independent to-manys into auditlog --- specifyweb/specify/api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 939fb9949c5..73a27897dcc 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -29,6 +29,7 @@ from .uiformatters import AutonumberOverflowException from .filter_by_col import filter_by_collection from .auditlog import auditlog +from .datamodel import datamodel from .calculated_fields import calculate_extra_fields ReadPermChecker = Callable[[Any], None] @@ -680,7 +681,12 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: to_remove.delete() else: for rel_obj in to_remove: - check_table_permissions(collection, agent, to_remove[0], 'update') + check_table_permissions(collection, agent, rel_obj, 'update') + related_field = datamodel.reverse_relationship(obj.specify_model.get_field_strict(field_name)) + if related_field is not None: + # REFACTOR: use fld_change_info + field_change_info: FieldChangeInfo = {"field_name": related_field.name, "old_value": obj.id, "new_value": None} + auditlog.update(rel_obj, agent, None, [field_change_info]) getattr(obj, field_name).remove(*list(to_remove)) def update_or_create_resource(collection, agent, model, data, parent_obj): From 067aaa6ae643ebe7b189b1cd7cd5711997ce4446 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 4 Sep 2024 16:46:23 -0500 Subject: [PATCH 035/132] Resolve backend tests --- specifyweb/specify/tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index 2646bbd1d66..ebb1d4dbdf1 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -665,8 +665,8 @@ def test_inline_error_handling(self): ) other_collection_object_data = { - 'id': self.collectionobjects[0].id, - 'catalognumber': self.collectionobjects[0].catalognumber, + 'id': self.collectionobjects[1].id, + 'catalognumber': self.collectionobjects[1].catalognumber, 'collection': api.uri_for_model('Collection', self.collection.id), 'determinations': [ api.uri_for_model('determination', new_determination.id) @@ -675,8 +675,8 @@ def test_inline_error_handling(self): with self.assertRaises(AssertionError): api.update_obj(self.collection, self.agent, - 'Collectionobject', self.collectionobjects[0].id, - self.collectionobjects[0].version, other_collection_object_data) + 'Collectionobject', self.collectionobjects[1].id, + self.collectionobjects[1].version, other_collection_object_data) # version control on inlined resources should be tested From f10e41062107053d082c5a0daa818b54d722ea53 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 4 Sep 2024 17:10:16 -0500 Subject: [PATCH 036/132] Resolve frontend tests --- .../lib/components/DataModel/scoping.ts | 36 +++++++++---------- .../lib/components/DataModel/specifyTable.ts | 1 + 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts b/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts index 20279182832..304640ff3ec 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts @@ -1,6 +1,5 @@ import type { RA } from '../../utils/types'; import { takeBetween } from '../../utils/utils'; -import { raise } from '../Errors/Crash'; import { getCollectionPref } from '../InitialContext/remotePrefs'; import { getTablePermissions } from '../Permissions'; import { hasTablePermission } from '../Permissions/helpers'; @@ -10,11 +9,11 @@ import type { AnySchema } from './helperTypes'; import type { SpecifyResource } from './legacyTypes'; import { getResourceApiUrl, idFromUrl } from './resource'; import { schema } from './schema'; +import { serializeResource } from './serializers'; import type { Relationship } from './specifyField'; import type { SpecifyTable } from './specifyTable'; import { strictGetTable, tables } from './tables'; -import type { CollectionObject } from './types'; -import type { Tables } from './types'; +import type { CollectionObject, Tables } from './types'; /** * Some tasks to do after a new resource is created @@ -51,27 +50,26 @@ export function initializeResource(resource: SpecifyResource): void { getCollectionPref('CO_CREATE_PREP', schema.domainLevelIds.collection) && hasTablePermission('Preparation', 'create') && resource.createdBy !== 'clone' - ) - collectionObject - .rgetCollection('preparations') - .then((preparations) => { - if (preparations.models.length === 0) - preparations.add(new tables.Preparation.Resource()); - }) - .catch(raise); + ) { + const preps = collectionObject.getDependentResource('preparations') ?? []; + if (preps.length === 0) + collectionObject.set('preparations', [ + serializeResource(new tables.Preparation.Resource()), + ]); + } if ( getCollectionPref('CO_CREATE_DET', schema.domainLevelIds.collection) && hasTablePermission('Determination', 'create') && resource.createdBy !== 'clone' - ) - collectionObject - .rgetCollection('determinations') - .then((determinations) => { - if (determinations.models.length === 0) - determinations.add(new tables.Determination.Resource()); - }) - .catch(raise); + ) { + const determinations = + collectionObject.getDependentResource('determinations') ?? []; + if (determinations.length === 0) + collectionObject.set('determinations', [ + serializeResource(new tables.Determination.Resource()), + ]); + } } export function getDomainResource< diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 43bcfdd833d..1eede1b49e0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -88,6 +88,7 @@ export type Collection = { readonly related?: SpecifyResource; readonly _totalCount?: number; readonly models: RA>; + readonly length: number; readonly table: { readonly specifyTable: SpecifyTable; }; From a6a233df0c71db31de2899c45c9db06e688d2632 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 9 Sep 2024 08:46:48 -0500 Subject: [PATCH 037/132] Make independent subview lazy --- .../lib/components/DataModel/collectionApi.ts | 176 ++++++++++-------- .../lib/components/DataModel/resourceApi.ts | 2 +- .../FormSliders/IntegratedRecordSelector.tsx | 12 +- .../RecordSelectorFromCollection.tsx | 5 +- .../js_src/lib/components/Forms/SubView.tsx | 6 +- specifyweb/specify/api.py | 103 ++++++++-- specifyweb/specify/tests/test_api.py | 57 +++--- 7 files changed, 236 insertions(+), 125 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 556161ca286..0276a11010a 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -2,8 +2,8 @@ import _ from 'underscore'; +import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; -import { formatUrl } from '../Router/queryString'; import { Backbone } from './backbone'; import type { AnySchema } from './helperTypes'; import type { SpecifyResource } from './legacyTypes'; @@ -92,78 +92,6 @@ export const DependentCollection = Base.extend({ create: notSupported, }); -export const IndependentCollection = Base.extend({ - __name__: 'IndependentCollectionBase', - constructor(options, records = []) { - this.table = this.model; - assert(_.isArray(records)); - Base.call(this, records, options); - this.filters = options.filters || {}; - this.domainfilter = - Boolean(options.domainfilter) && - this.model?.specifyTable.getScopingRelationship() !== undefined; - }, - initialize(_tables, options) { - this.on( - 'change add remove', - function (resource: SpecifyResource) { - this.trigger('saverequired'); - }, - this - ); - - setupToOne(this, options); - }, - url() { - return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; - }, - parse(resp) { - let records; - if (resp.meta) { - records = resp.objects; - } else { - console.warn("expected 'meta' in response"); - records = resp; - } - return records; - }, - async fetch(options) { - if (this.related.isNew() || this.related.isBeingInitialized()) { - return this; - } - - this.filters[this.field.name.toLowerCase()] = this.related.id; - if (this._fetch) return this._fetch; - - options = { ...(options ?? {}), silent: true }; - - assert(options.at == null); - - options.data = options.data || { - ...this.filters, - domainfilter: this.domainfilter, - limit: options.limit, - }; - const self = this; - this._fetch = Backbone.Collection.prototype.fetch.call(this, options); - return this._fetch.then(() => { - self._fetch = null; - return self; - }); - }, - isComplete() { - return true; - }, - toApiJSON(options) { - const self = this; - - return this.map(function (resource: SpecifyResource) { - const formatAsObject = resource.needsSaved || resource.isNew(); - return formatAsObject ? resource.toJSON(options) : resource.url(); - }); - }, -}); - export const LazyCollection = Base.extend({ __name__: 'LazyCollectionBase', _neverFetched: true, @@ -213,7 +141,7 @@ export const LazyCollection = Base.extend({ options.data = options.data || _.extend({ domainfilter: this.domainfilter }, this.filters); - options.data.offset = this.length; + options.data.offset = options.offset || this.length; _(options).has('limit') && (options.data.limit = options.limit); this._fetch = Backbone.Collection.prototype.fetch.call(this, options); @@ -233,6 +161,106 @@ export const LazyCollection = Base.extend({ }, }); +export const IndependentCollection = LazyCollection.extend({ + __name__: 'IndependentCollectionBase', + constructor(options, records = []) { + this.table = this.model; + assert(_.isArray(records)); + Base.call(this, records, options); + this.filters = options.filters || {}; + this.domainfilter = + Boolean(options.domainfilter) && + this.model?.specifyTable.getScopingRelationship() !== undefined; + + this.removed = new Set(); + this.updated = {}; + }, + initialize(_tables, options) { + this.on( + 'change', + function (resource: SpecifyResource) { + if (!resource.isBeingInitialized()) { + this.updated[resource.cid] = resource; + this.trigger('saverequired'); + } + }, + this + ); + + this.on( + 'add', + function (resource: SpecifyResource) { + if (!resource.isNew()) { + (this.removed as Set).delete(resource.url()); + this.updated[resource.cid] = resource.url(); + } else { + this.updated[resource.cid] = resource; + } + this._totalCount += 1; + this.trigger('saverequired'); + }, + this + ); + + this.on( + 'remove', + function (resource: SpecifyResource) { + if (!resource.isNew()) { + (this.removed as Set).add(resource.url()); + } + this.updated = removeKey(this.updated, resource.cid); + this._totalCount -= 1; + this.trigger('saverequired'); + }, + this + ); + + this.listenTo(options.related, 'saved', function () { + this.updated = {}; + this.removed = new Set(); + }); + + setupToOne(this, options); + }, + parse(resp) { + const self = this; + const records = Reflect.apply( + LazyCollection.prototype.parse, + this, + arguments + ); + + this._totalCount -= (this.removed as Set).size; + + return records.filter( + ({ resource_uri }) => !(this.removed as Set).has(resource_uri) + ); + }, + async fetch(options) { + if (this.related.isBeingInitialized()) { + return this; + } + this.filters[this.field.name.toLowerCase()] = this.related.id; + + const offset = + this.length === 0 && this.removed.size > 0 + ? this.removed.size + : this.length; + + options = { ...(options ?? {}), silent: true, offset }; + + return Reflect.apply(LazyCollection.prototype.fetch, this, [options]); + }, + toApiJSON(options) { + const self = this; + + return { + update: Object.values(this.updated), + remove: Array.from(self.removed), + }; + }, +}); + export const ToOneCollection = LazyCollection.extend({ __name__: 'LazyToOneCollectionBase', initialize(_models, options) { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 45a39644a57..f45fc1340e9 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -806,7 +806,7 @@ export const ResourceBase = Backbone.Model.extend({ ? new relatedTable.IndependentCollection(collectionOptions) : existingToMany; - return collection.fetch({ limit: 0 }).then((fetchedCollection) => { + return collection.fetch().then((fetchedCollection) => { this.storeIndependent(field, fetchedCollection); return fetchedCollection; }); diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 66d6037ff93..36e62af0487 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { State } from 'typesafe-reducer'; +import type { State } from 'typesafe-reducer'; import { useSearchParameter } from '../../hooks/navigation'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -75,7 +75,12 @@ export function IntegratedRecordSelector({ const [state, setState] = React.useState< | State< 'AddResourceState', - { readonly resource: SpecifyResource } + { + readonly resource: SpecifyResource; + readonly handleAdd: ( + resources: RA> + ) => void; + } > | State<'MainState'> >({ type: 'MainState' }); @@ -231,6 +236,7 @@ export function IntegratedRecordSelector({ setState({ type: 'AddResourceState', resource, + handleAdd, }); }} /> @@ -315,7 +321,7 @@ export function IntegratedRecordSelector({ onClose={(): void => setState({ type: 'MainState' })} onDeleted={undefined} onSaved={(): void => { - handleAdd([state.resource]); + state.handleAdd([state.resource]); setState({ type: 'MainState' }); }} /> diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 7c72ed933e5..5d0f0440ec4 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -5,6 +5,7 @@ import type { RA } from '../../utils/types'; import { defined } from '../../utils/types'; import { DependentCollection, + isRelationshipCollection, LazyCollection, } from '../DataModel/collectionApi'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -96,7 +97,9 @@ export function RecordSelectorFromCollection({ table: collection.table.specifyTable, field: relationship, records, - relatedResource: isLazy ? undefined : collection.related, + relatedResource: isRelationshipCollection(collection) + ? collection.related + : undefined, totalCount: collection._totalCount ?? records.length, onAdd: (rawResources): void => { const resources = isToOne ? rawResources.slice(0, 1) : rawResources; diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index ff0a60eb9c6..22279228fd6 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -263,7 +263,11 @@ export function SubView({ )} {typeof collection === 'object' && isOpen ? ( None: for field_name, val in list(data.items()): field = obj._meta.get_field(field_name) if not field.is_relation or (field.many_to_one or field.one_to_one): continue # Skip *-to-one fields. - is_dependent = is_dependent_field(obj, field_name) - - if not isinstance(val, list): - if is_dependent: - raise AssertionError("didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val)) - else: - # The field contains something other than nested data. - # Probably the URI of the collection - continue - + dependent = is_dependent_field(obj, field_name) + + if isinstance(val, list): + assert dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'), \ + "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) + elif hasattr(val, "items"): + assert not dependent, "got inline dictionary data for dependent field %s in %s: %r" % (field_name, obj, val) + else: + # The field contains something other than nested data. + # Probably the URI of the collection + continue + + if dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): + _handle_dependent_to_many(collection, agent, obj, field, val) + else: + _handle_independent_to_many(collection, agent, obj, field, val) + + return + rel_model = field.related_model ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. - + ids_to_fetch = [] cached_objs = dict() fk_model = None @@ -689,6 +698,74 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: auditlog.update(rel_obj, agent, None, [field_change_info]) getattr(obj, field_name).remove(*list(to_remove)) +def _handle_dependent_to_many(collection, agent, obj, field, value): + if not isinstance(value, list): + assert isinstance(value, list), "didn't get inline data for dependent field %s in %s: %r" % (field.name, obj, value) + + rel_model = field.related_model + ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. + + for rel_data in value: + rel_data[field.field.name] = obj + + rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj) + + ids.append(rel_obj.id) # Record the id as one to keep. + + # Delete related objects not in the ids list. + # TODO: Check versions for optimistic locking. + to_remove = getattr(obj, field.name).exclude(id__in=ids).select_for_update() + for rel_obj in to_remove: + check_table_permissions(collection, agent, rel_obj, "delete") + auditlog.remove(rel_obj, agent, obj) + + to_remove.delete() + +class IndependentInline(TypedDict): + update: List[Union[str, Dict[str, Any]]] + remove: List[str] + +def _handle_independent_to_many(collection, agent, obj, field, value: IndependentInline): + logger.warning("Updating independent collections via the API is experimental and the structure may be changed in the future") + + rel_model = field.related_model + + to_update = value.get('update', []) + to_remove = value.get('remove', []) + + ids_to_fetch = [] + cached_objs = dict() + fk_model = None + + # Fetch the related records which are provided as strings + for rel_data in [*to_update, *to_remove]: + if not isinstance(rel_data, str): continue + fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) + ids_to_fetch.append(fk_id) + + if fk_model is not None: + cached_objs = {item.id: obj_to_data(item) for item in get_model(fk_model).objects.filter(id__in=ids_to_fetch).select_for_update()} + + for rel_data in to_update: + if isinstance(rel_data, str): + fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) + rel_data = cached_objs[fk_id] + if rel_data[field.field.name] == uri_for_model(obj.__class__, obj.id): + continue + + rel_data[field.field.name] = obj + update_or_create_resource(collection, agent, rel_model, rel_data, None) + + if len(to_remove) > 0: + check_table_permissions(collection, agent, rel_model, 'update') + related_field = datamodel.reverse_relationship(obj.specify_model.get_field_strict(field.name)) + assert related_field is not None, f"no reverse relationship for {obj.__class__.__name__}.{field.field.name}" + for rel_obj in to_remove: + fk_model, fk_id = strict_uri_to_model(rel_obj, rel_model.__name__) + rel_data = cached_objs[fk_id] + rel_data[related_field.name] = None + update_obj(collection, agent, rel_model, rel_data["id"], rel_data["version"], rel_data) + def update_or_create_resource(collection, agent, model, data, parent_obj): if 'id' in data: return update_obj(collection, agent, diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index ebb1d4dbdf1..55e6ef677a9 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -498,10 +498,12 @@ def test_independent_to_many_set_inline(self): accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': [ - api.obj_to_data(self.collectionobjects[0]), - api.uri_for_model('collectionobject', self.collectionobjects[1].id) - ] + 'collectionobjects': { + "update": [ + api.obj_to_data(self.collectionobjects[0]), + api.uri_for_model('collectionobject', self.collectionobjects[1].id) + ] + } } accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) @@ -538,11 +540,13 @@ def test_indepenent_to_many_removing_from_inline(self): accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': [ - api.obj_to_data(collection_object) if index % 2 == 0 - else api.uri_for_model('collectionobject', collection_object.id) - for index, collection_object in enumerate(collection_objects_to_set) - ] + 'collectionobjects': { + "remove": [ + api.uri_for_model('collectionobject', collection_object.id) + for index, collection_object in enumerate(collection_objects_to_set) + if index % 2 == 0 + ] + } } accession = api.update_obj(self.collection, self.agent, 'Accession', accession.id, accession.version, accession_data) @@ -567,9 +571,11 @@ def test_updating_independent_to_many_resource(self): accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': [ + 'collectionobjects': { + "update": [ co_to_modify - ] + ] + } } self.assertEqual(self.collectionobjects[2].integer1, None) @@ -611,12 +617,14 @@ def test_independent_to_many_creating_from_remoteside(self): accession_data = { 'accessionnumber': "a", 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': [ + 'collectionobjects': { + "update": [ { 'catalognumber': new_catalognumber, 'collection': api.uri_for_model('Collection', self.collection.id) } - ] + ] + } } accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) @@ -636,10 +644,12 @@ def test_reassigning_independent_to_many(self): accession_data = { 'accessionnumber': "b", 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': [ + 'collectionobjects': { + "update": [ api.obj_to_data(self.collectionobjects[0]), api.uri_for_model('collectionobject', self.collectionobjects[1].id) - ] + ] + } } acc2 = api.create_obj(self.collection, self.agent, 'Accession', accession_data) self.collectionobjects[0].refresh_from_db() @@ -659,24 +669,7 @@ def test_inline_error_handling(self): api.update_obj(self.collection, self.agent, 'Collectionobject', self.collectionobjects[0].id, self.collectionobjects[0].version, collection_object_data) - - new_determination = models.Determination.objects.create( - collectionobject=self.collectionobjects[1] - ) - - other_collection_object_data = { - 'id': self.collectionobjects[1].id, - 'catalognumber': self.collectionobjects[1].catalognumber, - 'collection': api.uri_for_model('Collection', self.collection.id), - 'determinations': [ - api.uri_for_model('determination', new_determination.id) - ] - } - with self.assertRaises(AssertionError): - api.update_obj(self.collection, self.agent, - 'Collectionobject', self.collectionobjects[1].id, - self.collectionobjects[1].version, other_collection_object_data) # version control on inlined resources should be tested From 434047bed8ed721febc1264adeb86616b10c33a4 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 9 Sep 2024 09:07:36 -0500 Subject: [PATCH 038/132] Cleanup handle_to_many code --- specifyweb/specify/api.py | 51 --------------------------------------- 1 file changed, 51 deletions(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 4e8764843d3..4e3498a2ffc 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -647,57 +647,6 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: else: _handle_independent_to_many(collection, agent, obj, field, val) - return - - rel_model = field.related_model - ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. - - ids_to_fetch = [] - cached_objs = dict() - fk_model = None - if not is_dependent: - for rel_data in val: - if not isinstance(rel_data, str): continue - fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) - ids_to_fetch.append(fk_id) - - if fk_model is not None: - cached_objs = {item.id: obj_to_data(item) for item in get_model(fk_model).objects.filter(id__in=ids_to_fetch)} - - for rel_data in val: - if isinstance(rel_data, str): - assert not is_dependent, "expected object for dependent field %s in %s: %s" % (field_name, obj, rel_data) - fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) - rel_data = cached_objs[fk_id] - if rel_data[field.field.name] == uri_for_model(obj.__class__, obj.id): - ids.append(rel_data["id"]) - continue - - rel_data[field.field.name] = obj - - rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj if is_dependent else None) - - ids.append(rel_obj.id) # Record the id as one to keep. - - # Delete related objects not in the ids list. - # TODO: Check versions for optimistic locking. - to_remove = getattr(obj, field_name).exclude(id__in=ids).select_for_update() - if is_dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): - for rel_obj in to_remove: - check_table_permissions(collection, agent, rel_obj, "delete") - auditlog.remove(rel_obj, agent, obj) - - to_remove.delete() - else: - for rel_obj in to_remove: - check_table_permissions(collection, agent, rel_obj, 'update') - related_field = datamodel.reverse_relationship(obj.specify_model.get_field_strict(field_name)) - if related_field is not None: - # REFACTOR: use fld_change_info - field_change_info: FieldChangeInfo = {"field_name": related_field.name, "old_value": obj.id, "new_value": None} - auditlog.update(rel_obj, agent, None, [field_change_info]) - getattr(obj, field_name).remove(*list(to_remove)) - def _handle_dependent_to_many(collection, agent, obj, field, value): if not isinstance(value, list): assert isinstance(value, list), "didn't get inline data for dependent field %s in %s: %r" % (field.name, obj, value) From febbc16bce65eb9b6a8d5379ec27c9f0b9bcbcf1 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 11 Sep 2024 09:20:19 -0500 Subject: [PATCH 039/132] Cleanup API for to-many Collections --- specifyweb/specify/api.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 4e3498a2ffc..2d46539f575 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -651,24 +651,24 @@ def _handle_dependent_to_many(collection, agent, obj, field, value): if not isinstance(value, list): assert isinstance(value, list), "didn't get inline data for dependent field %s in %s: %r" % (field.name, obj, value) - rel_model = field.related_model - ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. + rel_model = field.related_model + ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. - for rel_data in value: - rel_data[field.field.name] = obj + for rel_data in value: + rel_data[field.field.name] = obj - rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj) + rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj) - ids.append(rel_obj.id) # Record the id as one to keep. + ids.append(rel_obj.id) # Record the id as one to keep. - # Delete related objects not in the ids list. - # TODO: Check versions for optimistic locking. - to_remove = getattr(obj, field.name).exclude(id__in=ids).select_for_update() - for rel_obj in to_remove: - check_table_permissions(collection, agent, rel_obj, "delete") - auditlog.remove(rel_obj, agent, obj) - - to_remove.delete() + # Delete related objects not in the ids list. + # TODO: Check versions for optimistic locking. + to_remove = getattr(obj, field.name).exclude(id__in=ids).select_for_update() + for rel_obj in to_remove: + check_table_permissions(collection, agent, rel_obj, "delete") + auditlog.remove(rel_obj, agent, obj) + + to_remove.delete() class IndependentInline(TypedDict): update: List[Union[str, Dict[str, Any]]] @@ -686,15 +686,17 @@ def _handle_independent_to_many(collection, agent, obj, field, value: Independen cached_objs = dict() fk_model = None + to_fetch = [*to_update, *to_remove] + # Fetch the related records which are provided as strings - for rel_data in [*to_update, *to_remove]: + for rel_data in to_fetch: if not isinstance(rel_data, str): continue fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) ids_to_fetch.append(fk_id) if fk_model is not None: cached_objs = {item.id: obj_to_data(item) for item in get_model(fk_model).objects.filter(id__in=ids_to_fetch).select_for_update()} - + for rel_data in to_update: if isinstance(rel_data, str): fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) @@ -706,12 +708,13 @@ def _handle_independent_to_many(collection, agent, obj, field, value: Independen update_or_create_resource(collection, agent, rel_model, rel_data, None) if len(to_remove) > 0: - check_table_permissions(collection, agent, rel_model, 'update') + assert obj.pk is not None, f"Unable to remove {obj.__class__.__name__}.{field.field.name} resources from new {obj.__class__.__name__}" related_field = datamodel.reverse_relationship(obj.specify_model.get_field_strict(field.name)) assert related_field is not None, f"no reverse relationship for {obj.__class__.__name__}.{field.field.name}" for rel_obj in to_remove: fk_model, fk_id = strict_uri_to_model(rel_obj, rel_model.__name__) rel_data = cached_objs[fk_id] + assert rel_data[related_field.name] == uri_for_model(obj.__class__, obj.pk), f"Related {related_field.relatedModelName} does not belong to {obj.__class__.__name__}.{field.field.name}: {rel_obj}" rel_data[related_field.name] = None update_obj(collection, agent, rel_model, rel_data["id"], rel_data["version"], rel_data) From 794fdee44594aa63ea823a3b4ca3cbeb5cdb8103 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 11 Sep 2024 09:28:12 -0500 Subject: [PATCH 040/132] Don't automatically fetch Independent ToOne Collections when modified --- .../components/FormSliders/RecordSelectorFromCollection.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 5d0f0440ec4..2268b1377d3 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -5,6 +5,7 @@ import type { RA } from '../../utils/types'; import { defined } from '../../utils/types'; import { DependentCollection, + IndependentCollection, isRelationshipCollection, LazyCollection, } from '../DataModel/collectionApi'; @@ -80,6 +81,7 @@ export function RecordSelectorFromCollection({ * don't need to fetch all records in between) */ if ( + !isToOne && isLazy && collection.related?.isNew() !== true && !collection.isComplete() && @@ -89,7 +91,7 @@ export function RecordSelectorFromCollection({ .fetch() .then(() => setRecords(getRecords)) .catch(raise); - }, [collection, isLazy, getRecords, index, records.length]); + }, [collection, isLazy, getRecords, index, records.length, isToOne]); const state = useRecordSelector({ ...rest, From 872f6019217d6c2a5f7c2a580fc414ae9ade3a08 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 12 Sep 2024 14:37:02 -0500 Subject: [PATCH 041/132] Initialize totalCount variable for lazy collections --- .../js_src/lib/components/DataModel/collectionApi.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 0276a11010a..2b177574c84 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -98,6 +98,8 @@ export const LazyCollection = Base.extend({ constructor(options = {}) { this.table = this.model; Base.call(this, null, options); + this._neverFetched = true; + this._totalCount = undefined; this.filters = options.filters || {}; this.domainfilter = Boolean(options.domainfilter) && @@ -107,7 +109,7 @@ export const LazyCollection = Base.extend({ return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; }, isComplete() { - return this.length === this._totalCount; + return this._neverFetched && this.length === this._totalCount; }, parse(resp) { let objects; @@ -172,6 +174,8 @@ export const IndependentCollection = LazyCollection.extend({ Boolean(options.domainfilter) && this.model?.specifyTable.getScopingRelationship() !== undefined; + this._totalCount = records.length; + this.removed = new Set(); this.updated = {}; }, @@ -232,9 +236,7 @@ export const IndependentCollection = LazyCollection.extend({ this._totalCount -= (this.removed as Set).size; - return records.filter( - ({ resource_uri }) => !(this.removed as Set).has(resource_uri) - ); + return records; }, async fetch(options) { if (this.related.isBeingInitialized()) { From 868e76a460a3fd9f7d9da2db1d477d320a751deb Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 13 Sep 2024 06:59:48 -0700 Subject: [PATCH 042/132] Remove import + chnage url test --- .../lib/components/DataModel/__tests__/resourceApi.test.ts | 2 +- .../lib/components/FormSliders/RecordSelectorFromCollection.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 62f09a5fac7..57c4118a250 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -147,7 +147,7 @@ const accessionsResponse = [ ]; overrideAjax( - '/api/specify/accession/?addressofrecord=42&domainfilter=false&limit=0', + '/api/specify/accession/?domainfilter=false&addressofrecord=42&offset=0', { meta: { total_count: 2 }, objects: accessionsResponse, diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 2268b1377d3..2bc987ee9cb 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -5,7 +5,6 @@ import type { RA } from '../../utils/types'; import { defined } from '../../utils/types'; import { DependentCollection, - IndependentCollection, isRelationshipCollection, LazyCollection, } from '../DataModel/collectionApi'; From 387aa328496309d92e41c140aaa9a55ba4e74202 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 13 Sep 2024 15:00:41 -0400 Subject: [PATCH 043/132] Add cogtype helpers --- .../frontend/js_src/lib/components/DataModel/helpers.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts index 8ccdfdb534b..a3e1cbfdfbf 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts @@ -216,3 +216,10 @@ export async function fetchDistantRelated( field, }; } + +// Cog types: Discrete, Consolidated, Drill Core +export const cogTypes = { + DISCRETE: 'Discrete', + CONSOLIDATED: 'Consolidated', + DRILL_CORE: 'Drill Core' +} \ No newline at end of file From c1e8978f5ccc54f8b827d70949605a24c44bc724 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 13 Sep 2024 15:00:53 -0400 Subject: [PATCH 044/132] Add COG business rules --- .../components/DataModel/businessRuleDefs.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 8e98a9ec793..c902f9fe72f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -2,6 +2,7 @@ import { formsText } from '../../localization/forms'; import { resourcesText } from '../../localization/resources'; import { f } from '../../utils/functools'; import type { BusinessRuleResult } from './businessRules'; +import { cogTypes } from './helpers'; import type { AnySchema, TableFields } from './helperTypes'; import { checkPrepAvailability, @@ -20,6 +21,8 @@ import type { Address, BorrowMaterial, CollectionObject, + CollectionObjectGroup, + CollectionObjectGroupJoin, Determination, DNASequence, LoanPreparation, @@ -202,6 +205,54 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }, }, + CollectionObjectGroup: { + fieldChecks: { + cogtype: (cog: SpecifyResource) => { + // The first COJO CO will automatically have isPrimary set to True when the COG type is 'consolidated' + cog.rgetPromise('cogtype').then((cogtype) => { + if (cogtype.get('type') === cogTypes.CONSOLIDATED) { + const cojos = cog.getDependentResource('cojo'); + // Set first CO in COG to primary + cojos?.models + .filter((cojo) => cojo.get('childco'))[0] + .set('isprimary', true); + } + }); + }, + }, + }, + + CollectionObjectGroupJoin: { + fieldChecks: { + // Only a single CO in a COG can be set as primary. + // When checking a CO as primary, other COs in that COG will get unchecked. + isprimary: (cojo: SpecifyResource) => { + if (cojo.get('isprimary') && cojo.collection !== undefined) { + cojo.collection.models + .filter((resource) => resource.get('childco')) + .map((other: SpecifyResource) => { + if (other.cid !== cojo.cid) { + other.set('isprimary', false); + } + }); + } + }, + // Only a single CO in a COG can be set as substrate. + // When checking a CO as substrate, other COs in that COG will get unchecked. + issubstrate: (cojo: SpecifyResource) => { + if (cojo.get('issubstrate') && cojo.collection !== undefined) { + cojo.collection.models + .filter((resource) => resource.get('childco')) + .map((other: SpecifyResource) => { + if (other.cid !== cojo.cid) { + other.set('issubstrate', false); + } + }); + } + }, + }, + }, + Determination: { fieldChecks: { taxon: async ( From fda0c2962c0a250062613d9dac24fc4d39b14087 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 13 Sep 2024 19:05:38 +0000 Subject: [PATCH 045/132] Lint code with ESLint and Prettier Triggered by c1e8978f5ccc54f8b827d70949605a24c44bc724 on branch refs/heads/issue-5246 --- .../lib/components/DataModel/businessRuleDefs.ts | 14 +++++++++----- .../js_src/lib/components/DataModel/helpers.ts | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index c902f9fe72f..d7697f8a896 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -214,7 +214,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { const cojos = cog.getDependentResource('cojo'); // Set first CO in COG to primary cojos?.models - .filter((cojo) => cojo.get('childco'))[0] + .find((cojo) => cojo.get('childco')) .set('isprimary', true); } }); @@ -224,8 +224,10 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { CollectionObjectGroupJoin: { fieldChecks: { - // Only a single CO in a COG can be set as primary. - // When checking a CO as primary, other COs in that COG will get unchecked. + /* + * Only a single CO in a COG can be set as primary. + * When checking a CO as primary, other COs in that COG will get unchecked. + */ isprimary: (cojo: SpecifyResource) => { if (cojo.get('isprimary') && cojo.collection !== undefined) { cojo.collection.models @@ -237,8 +239,10 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }); } }, - // Only a single CO in a COG can be set as substrate. - // When checking a CO as substrate, other COs in that COG will get unchecked. + /* + * Only a single CO in a COG can be set as substrate. + * When checking a CO as substrate, other COs in that COG will get unchecked. + */ issubstrate: (cojo: SpecifyResource) => { if (cojo.get('issubstrate') && cojo.collection !== undefined) { cojo.collection.models diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts index a3e1cbfdfbf..3a09c959b1f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts @@ -221,5 +221,5 @@ export async function fetchDistantRelated( export const cogTypes = { DISCRETE: 'Discrete', CONSOLIDATED: 'Consolidated', - DRILL_CORE: 'Drill Core' -} \ No newline at end of file + DRILL_CORE: 'Drill Core', +}; From 22d057f5bb30366e6c4a57f6c67f1df1e7db7848 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 16 Sep 2024 10:46:05 -0500 Subject: [PATCH 046/132] Indep-to-many: use Django fieldnames over datamodel field names --- specifyweb/specify/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 2d46539f575..56dd8e72cc2 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -714,8 +714,8 @@ def _handle_independent_to_many(collection, agent, obj, field, value: Independen for rel_obj in to_remove: fk_model, fk_id = strict_uri_to_model(rel_obj, rel_model.__name__) rel_data = cached_objs[fk_id] - assert rel_data[related_field.name] == uri_for_model(obj.__class__, obj.pk), f"Related {related_field.relatedModelName} does not belong to {obj.__class__.__name__}.{field.field.name}: {rel_obj}" - rel_data[related_field.name] = None + assert rel_data[field.field.name] == uri_for_model(obj.__class__, obj.pk), f"Related {related_field.relatedModelName} does not belong to {obj.__class__.__name__}.{field.field.name}: {rel_obj}" + rel_data[field.field.name] = None update_obj(collection, agent, rel_model, rel_data["id"], rel_data["version"], rel_data) def update_or_create_resource(collection, agent, model, data, parent_obj): From 381cefc1a6b38c0d0422d03e7de63f4ee6955f7c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 16 Sep 2024 16:40:58 -0400 Subject: [PATCH 047/132] Generate new types - frontend and backend fieldNames were out of sync --- .../js_src/lib/components/DataModel/types.ts | 180 +++++++++--------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index 01deed86547..66d0fdddfc4 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -3,8 +3,8 @@ * Afterward, some manual edits have been made. Those are marked with * "NOTE:" comments * - * Schema version: 2.11 - * Date generated: July 17, 2024 + * Schema version: 2.12 + * Date generated: September 16, 2024 * * The dataModel types were generated using the following code snippet. * After schema changes, it needs to be regenerated like this: @@ -233,20 +233,20 @@ export type Tables = { export type Accession = { readonly tableName: 'Accession'; readonly fields: { - readonly accessionNumber: string; readonly accessionCondition: string | null; - readonly dateAccessioned: string | null; + readonly accessionNumber: string; readonly actualTotalCountAmt: number | null; readonly collectionObjectCount: number | null; + readonly dateAccessioned: string | null; readonly dateAcknowledged: string | null; - readonly remarks: string | null; + readonly dateReceived: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; readonly number1: number | null; readonly number2: number | null; readonly preparationCount: number | null; - readonly dateReceived: string | null; + readonly remarks: string | null; readonly status: string | null; readonly text1: string | null; readonly text2: string | null; @@ -452,10 +452,10 @@ export type Agent = { readonly initials: string | null; readonly integer1: number | null; readonly integer2: number | null; + readonly interests: string | null; readonly jobTitle: string | null; readonly lastName: string | null; readonly middleInitial: string | null; - readonly interests: string | null; readonly remarks: string | null; readonly suffix: string | null; readonly text1: string | null; @@ -488,8 +488,8 @@ export type Agent = { readonly agentAttachments: RA; readonly agentGeographies: RA; readonly agentSpecialties: RA; - readonly identifiers: RA; readonly groups: RA; + readonly identifiers: RA; readonly variants: RA; }; readonly toManyIndependent: { @@ -920,13 +920,13 @@ export type BorrowMaterial = { readonly tableName: 'BorrowMaterial'; readonly fields: { readonly collectionMemberId: number; + readonly description: string | null; readonly inComments: string | null; readonly materialNumber: string; readonly outComments: string | null; readonly quantity: number | null; readonly quantityResolved: number | null; readonly quantityReturned: number | null; - readonly description: string | null; readonly text1: string | null; readonly text2: string | null; readonly timestampCreated: string; @@ -968,22 +968,24 @@ export type BorrowReturnMaterial = { export type CollectingEvent = { readonly tableName: 'CollectingEvent'; readonly fields: { - readonly startDate: string | null; + readonly verbatimDate: string | null; + readonly remarks: string | null; readonly endDate: string | null; readonly endDatePrecision: number | null; readonly endDateVerbatim: string | null; readonly endTime: number | null; - readonly stationFieldNumber: string | null; - readonly method: string | null; readonly guid: string | null; readonly integer1: number | null; readonly integer2: number | null; - readonly remarks: string | null; + readonly stationFieldNumber: string | null; + readonly verbatimLocality: string | null; + readonly method: string | null; readonly reservedInteger3: number | null; readonly reservedInteger4: number | null; readonly reservedText1: string | null; readonly reservedText2: string | null; readonly sgrStatus: number | null; + readonly startDate: string | null; readonly startDatePrecision: number | null; readonly startDateVerbatim: string | null; readonly startTime: number | null; @@ -1001,8 +1003,6 @@ export type CollectingEvent = { readonly timestampCreated: string; readonly timestampModified: string | null; readonly uniqueIdentifier: string | null; - readonly verbatimDate: string | null; - readonly verbatimLocality: string | null; readonly version: number | null; readonly visibility: number | null; }; @@ -1070,10 +1070,6 @@ export type CollectingEventAttr = { export type CollectingEventAttribute = { readonly tableName: 'CollectingEventAttribute'; readonly fields: { - readonly text8: string | null; - readonly text5: string | null; - readonly text4: string | null; - readonly text9: string | null; readonly integer1: number | null; readonly integer10: number | null; readonly integer2: number | null; @@ -1084,11 +1080,11 @@ export type CollectingEventAttribute = { readonly integer7: number | null; readonly integer8: number | null; readonly integer9: number | null; - readonly number12: number | null; - readonly number13: number | null; readonly number1: number | null; readonly number10: number | null; readonly number11: number | null; + readonly number12: number | null; + readonly number13: number | null; readonly number2: number | null; readonly number3: number | null; readonly number4: number | null; @@ -1098,22 +1094,26 @@ export type CollectingEventAttribute = { readonly number8: number | null; readonly number9: number | null; readonly remarks: string | null; - readonly text6: string | null; readonly text1: string | null; readonly text10: string | null; readonly text11: string | null; + readonly text12: string | null; readonly text13: string | null; readonly text14: string | null; readonly text15: string | null; readonly text16: string | null; readonly text17: string | null; readonly text2: string | null; + readonly text3: string | null; + readonly text4: string | null; + readonly text5: string | null; + readonly text6: string | null; readonly text7: string | null; + readonly text8: string | null; + readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly text12: string | null; readonly version: number | null; - readonly text3: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; @@ -1154,7 +1154,6 @@ export type CollectingTrip = { readonly tableName: 'CollectingTrip'; readonly fields: { readonly cruise: string | null; - readonly text2: string | null; readonly date1: string | null; readonly date1Precision: number | null; readonly date2: string | null; @@ -1172,6 +1171,8 @@ export type CollectingTrip = { readonly startDatePrecision: number | null; readonly startDateVerbatim: string | null; readonly startTime: number | null; + readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -1183,7 +1184,6 @@ export type CollectingTrip = { readonly timestampModified: string | null; readonly collectingTripName: string | null; readonly version: number | null; - readonly text1: string | null; readonly vessel: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; @@ -1359,15 +1359,18 @@ export type Collection = { export type CollectionObject = { readonly tableName: 'CollectionObject'; readonly fields: { + readonly yesNo1: boolean | null; readonly actualTotalCountAmt: number | null; + readonly altCatalogNumber: string | null; readonly availability: string | null; - readonly catalogNumber: string | null; readonly catalogedDate: string | null; readonly catalogedDatePrecision: number | null; readonly catalogedDateVerbatim: string | null; + readonly catalogNumber: string | null; + readonly remarks: string | null; readonly collectionMemberId: number; readonly countAmt: number | null; - readonly reservedText: string | null; + readonly timestampCreated: string; readonly timestampModified: string | null; readonly date1: string | null; readonly date1Precision: number | null; @@ -1380,9 +1383,10 @@ export type CollectionObject = { readonly guid: string | null; readonly integer1: number | null; readonly integer2: number | null; - readonly text2: string | null; readonly inventoryDate: string | null; readonly inventoryDatePrecision: number | null; + readonly text2: string | null; + readonly fieldNumber: string | null; readonly modifier: string | null; readonly name: string | null; readonly notifications: string | null; @@ -1391,16 +1395,15 @@ export type CollectionObject = { readonly number2: number | null; readonly objectCondition: string | null; readonly ocr: string | null; - readonly altCatalogNumber: string | null; + readonly text1: string | null; readonly projectNumber: string | null; - readonly remarks: string | null; readonly reservedInteger3: number | null; readonly reservedInteger4: number | null; + readonly reservedText: string | null; readonly reservedText2: string | null; readonly reservedText3: string | null; readonly restrictions: string | null; readonly sgrStatus: number | null; - readonly text1: string | null; readonly description: string | null; readonly text3: string | null; readonly text4: string | null; @@ -1408,14 +1411,11 @@ export type CollectionObject = { readonly text6: string | null; readonly text7: string | null; readonly text8: string | null; - readonly timestampCreated: string; readonly totalCountAmt: number | null; readonly totalValue: number | null; readonly uniqueIdentifier: string | null; readonly version: number | null; readonly visibility: number | null; - readonly fieldNumber: string | null; - readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; readonly yesNo4: boolean | null; @@ -1430,17 +1430,17 @@ export type CollectionObject = { readonly agent1: Agent | null; readonly appraisal: Appraisal | null; readonly cataloger: Agent | null; + readonly collectingEvent: CollectingEvent | null; readonly collection: Collection; readonly collectionObjectType: CollectionObjectType; readonly container: Container | null; readonly containerOwner: Container | null; readonly createdByAgent: Agent | null; readonly currentDetermination: Determination | null; - readonly modifiedByAgent: Agent | null; readonly embargoAuthority: Agent | null; - readonly collectingEvent: CollectingEvent | null; readonly fieldNotebookPage: FieldNotebookPage | null; readonly inventorizedBy: Agent | null; + readonly modifiedByAgent: Agent | null; readonly paleoContext: PaleoContext | null; readonly visibilitySetBy: SpecifyUser | null; }; @@ -1521,11 +1521,11 @@ export type CollectionObjectAttribute = { readonly integer7: number | null; readonly integer8: number | null; readonly integer9: number | null; - readonly number12: number | null; - readonly number13: number | null; readonly number1: number | null; readonly number10: number | null; readonly number11: number | null; + readonly number12: number | null; + readonly number13: number | null; readonly number14: number | null; readonly number15: number | null; readonly number16: number | null; @@ -1563,19 +1563,20 @@ export type CollectionObjectAttribute = { readonly number7: number | null; readonly number8: number | null; readonly number9: number | null; - readonly text13: string | null; - readonly text14: string | null; - readonly text1: string | null; readonly positionState: string | null; - readonly text10: string | null; readonly remarks: string | null; - readonly text8: string | null; + readonly text1: string | null; + readonly text10: string | null; readonly text11: string | null; + readonly text12: string | null; + readonly text13: string | null; + readonly text14: string | null; readonly text15: string | null; readonly text16: string | null; readonly text17: string | null; readonly text18: string | null; readonly text19: string | null; + readonly text2: string | null; readonly text20: string | null; readonly text21: string | null; readonly text22: string | null; @@ -1602,13 +1603,12 @@ export type CollectionObjectAttribute = { readonly text5: string | null; readonly text6: string | null; readonly text7: string | null; + readonly text8: string | null; readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly text12: string | null; readonly topDistance: number | null; readonly version: number | null; - readonly text2: string | null; readonly yesNo1: boolean | null; readonly yesNo10: boolean | null; readonly yesNo11: boolean | null; @@ -1646,10 +1646,10 @@ export type CollectionObjectCitation = { readonly fields: { readonly collectionMemberId: number; readonly figureNumber: string | null; + readonly remarks: string | null; readonly isFigured: boolean | null; readonly pageNumber: string | null; readonly plateNumber: string | null; - readonly remarks: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly version: number | null; @@ -2182,9 +2182,7 @@ export type DNASequence = { readonly compT: number | null; readonly extractionDate: string | null; readonly extractionDatePrecision: number | null; - readonly text2: string | null; readonly genbankAccessionNumber: string | null; - readonly text1: string | null; readonly geneSequence: string | null; readonly moleculeType: string | null; readonly number1: number | null; @@ -2194,6 +2192,8 @@ export type DNASequence = { readonly sequenceDate: string | null; readonly sequenceDatePrecision: number | null; readonly targetMarker: string | null; + readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; @@ -2286,8 +2286,8 @@ export type DNASequencingRun = { readonly runByAgent: Agent | null; }; readonly toManyDependent: { - readonly citations: RA; readonly attachments: RA; + readonly citations: RA; }; readonly toManyIndependent: RR; }; @@ -2449,13 +2449,11 @@ export type Determination = { readonly addendum: string | null; readonly alternateName: string | null; readonly collectionMemberId: number; - readonly confidence: string | null; readonly isCurrent: boolean; readonly determinedDate: string | null; readonly determinedDatePrecision: number | null; readonly featureOrBasis: string | null; readonly guid: string | null; - readonly yesNo1: boolean | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -2468,10 +2466,11 @@ export type Determination = { readonly number3: number | null; readonly number4: number | null; readonly number5: number | null; + readonly text1: string | null; readonly qualifier: string | null; + readonly confidence: string | null; readonly remarks: string | null; readonly subSpQualifier: string | null; - readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; @@ -2484,6 +2483,7 @@ export type Determination = { readonly typeStatusName: string | null; readonly varQualifier: string | null; readonly version: number | null; + readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; readonly yesNo4: boolean | null; @@ -2510,9 +2510,9 @@ export type DeterminationCitation = { readonly collectionMemberId: number; readonly figureNumber: string | null; readonly isFigured: boolean | null; + readonly remarks: string | null; readonly pageNumber: string | null; readonly plateNumber: string | null; - readonly remarks: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly version: number | null; @@ -3079,12 +3079,13 @@ export type FundingAgent = { export type GeoCoordDetail = { readonly tableName: 'GeoCoordDetail'; readonly fields: { + readonly validation: string | null; + readonly source: string | null; readonly errorPolygon: string | null; readonly geoRefAccuracy: number | null; readonly geoRefAccuracyUnits: string | null; readonly geoRefCompiledDate: string | null; readonly geoRefDetDate: string | null; - readonly geoRefDetRef: string | null; readonly geoRefRemarks: string | null; readonly geoRefVerificationStatus: string | null; readonly integer1: number | null; @@ -3103,7 +3104,7 @@ export type GeoCoordDetail = { readonly number5: number | null; readonly originalCoordSystem: string | null; readonly protocol: string | null; - readonly source: string | null; + readonly geoRefDetRef: string | null; readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; @@ -3112,7 +3113,6 @@ export type GeoCoordDetail = { readonly timestampCreated: string; readonly timestampModified: string | null; readonly uncertaintyPolygon: string | null; - readonly validation: string | null; readonly version: number | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; @@ -3334,8 +3334,8 @@ export type Gift = { readonly srcTaxonomy: string | null; readonly specialConditions: string | null; readonly status: string | null; - readonly text2: string | null; readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3704,7 +3704,6 @@ export type Loan = { readonly dateClosed: string | null; readonly dateReceived: string | null; readonly yesNo1: boolean | null; - readonly text2: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -3726,6 +3725,7 @@ export type Loan = { readonly specialConditions: string | null; readonly status: string | null; readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3851,22 +3851,26 @@ export type LoanReturnPreparation = { export type Locality = { readonly tableName: 'Locality'; readonly fields: { + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly datum: string | null; readonly elevationAccuracy: number | null; + readonly elevationMethod: string | null; readonly gml: string | null; readonly guid: string | null; readonly latLongMethod: string | null; readonly lat1text: string | null; readonly lat2text: string | null; - readonly latLongAccuracy: number | null; readonly latLongType: string | null; readonly latitude1: number | null; readonly latitude2: number | null; + readonly latLongAccuracy: number | null; readonly localityName: string; readonly long1text: string | null; readonly long2text: string | null; readonly longitude1: number | null; readonly longitude2: number | null; + readonly text1: string | null; readonly maxElevation: number | null; readonly minElevation: number | null; readonly namedPlace: string | null; @@ -3877,20 +3881,16 @@ export type Locality = { readonly sgrStatus: number | null; readonly shortName: string | null; readonly srcLatLongUnit: number; - readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; - readonly timestampCreated: string; - readonly timestampModified: string | null; readonly uniqueIdentifier: string | null; readonly verbatimElevation: string | null; readonly verbatimLatitude: string | null; readonly verbatimLongitude: string | null; readonly version: number | null; readonly visibility: number | null; - readonly elevationMethod: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; @@ -3963,7 +3963,6 @@ export type LocalityCitation = { export type LocalityDetail = { readonly tableName: 'LocalityDetail'; readonly fields: { - readonly baseMeridian: string | null; readonly drainage: string | null; readonly endDepth: number | null; readonly endDepthUnit: string | null; @@ -3972,6 +3971,7 @@ export type LocalityDetail = { readonly hucCode: string | null; readonly island: string | null; readonly islandGroup: string | null; + readonly text1: string | null; readonly mgrsZone: string | null; readonly nationalParkName: string | null; readonly number1: number | null; @@ -3988,7 +3988,7 @@ export type LocalityDetail = { readonly startDepth: number | null; readonly startDepthUnit: string | null; readonly startDepthVerbatim: string | null; - readonly text1: string | null; + readonly baseMeridian: string | null; readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; @@ -4165,6 +4165,7 @@ export type PaleoContext = { readonly tableName: 'PaleoContext'; readonly fields: { readonly text1: string | null; + readonly text2: string | null; readonly paleoContextName: string | null; readonly number1: number | null; readonly number2: number | null; @@ -4172,7 +4173,6 @@ export type PaleoContext = { readonly number4: number | null; readonly number5: number | null; readonly remarks: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -4371,6 +4371,7 @@ export type Preparation = { readonly date4Precision: number | null; readonly description: string | null; readonly guid: string | null; + readonly text1: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly isOnLoan: boolean | null; @@ -4388,6 +4389,7 @@ export type Preparation = { readonly text11: string | null; readonly text12: string | null; readonly text13: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -4397,10 +4399,8 @@ export type Preparation = { readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly text1: string | null; - readonly yesNo1: boolean | null; readonly version: number | null; - readonly text2: string | null; + readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; }; @@ -4777,10 +4777,7 @@ export type RecordSetItem = { export type ReferenceWork = { readonly tableName: 'ReferenceWork'; readonly fields: { - readonly text1: string | null; - readonly workDate: string | null; readonly doi: string | null; - readonly text2: string | null; readonly guid: string | null; readonly isPublished: boolean | null; readonly isbn: string | null; @@ -4791,6 +4788,8 @@ export type ReferenceWork = { readonly placeOfPublication: string | null; readonly publisher: string | null; readonly remarks: string | null; + readonly text1: string | null; + readonly text2: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly title: string; @@ -4799,6 +4798,7 @@ export type ReferenceWork = { readonly url: string | null; readonly version: number | null; readonly volume: string | null; + readonly workDate: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; }; @@ -4896,9 +4896,9 @@ export type RepositoryAgreementAttachment = { export type Shipment = { readonly tableName: 'Shipment'; readonly fields: { - readonly numberOfPackages: number | null; readonly insuredForAmount: string | null; readonly shipmentMethod: string | null; + readonly numberOfPackages: number | null; readonly number1: number | null; readonly number2: number | null; readonly remarks: string | null; @@ -5612,6 +5612,7 @@ export type Taxon = { readonly colStatus: string | null; readonly commonName: string | null; readonly cultivarName: string | null; + readonly environmentalProtectionStatus: string | null; readonly esaStatus: string | null; readonly fullName: string | null; readonly groupNumber: string | null; @@ -5635,7 +5636,6 @@ export type Taxon = { readonly number3: number | null; readonly number4: number | null; readonly number5: number | null; - readonly environmentalProtectionStatus: string | null; readonly rankId: number; readonly remarks: string | null; readonly source: string | null; @@ -6460,8 +6460,8 @@ export type CollectionObjectType = { readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; - readonly timestampcreated: string; - readonly timestampmodified: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly version: number | null; }; readonly toOneDependent: RR; @@ -6490,8 +6490,8 @@ export type CollectionObjectGroup = { readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; - readonly timestampcreated: string; - readonly timestampmodified: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly version: number | null; readonly yesno1: boolean | null; readonly yesno2: boolean | null; @@ -6499,14 +6499,14 @@ export type CollectionObjectGroup = { }; readonly toOneDependent: RR; readonly toOneIndependent: { - readonly cogtype: CollectionObjectGroupType; + readonly cogType: CollectionObjectGroupType; readonly collection: Collection | null; readonly createdByAgent: Agent | null; readonly modifiedByAgent: Agent | null; }; readonly toManyDependent: { readonly cojo: RA; - readonly parentcojos: RA; + readonly parentCojos: RA; }; readonly toManyIndependent: RR; }; @@ -6516,14 +6516,14 @@ export type CollectionObjectGroupJoin = { readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; - readonly isprimary: boolean; - readonly issubstrate: boolean; + readonly isPrimary: boolean; + readonly isSubstrate: boolean; readonly precedence: number; readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; - readonly timestampcreated: string; - readonly timestampmodified: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly version: number | null; readonly yesno1: boolean | null; readonly yesno2: boolean | null; @@ -6531,9 +6531,9 @@ export type CollectionObjectGroupJoin = { }; readonly toOneDependent: RR; readonly toOneIndependent: { - readonly childco: CollectionObject | null; - readonly childcog: CollectionObjectGroup | null; - readonly parentcog: CollectionObjectGroup; + readonly childCo: CollectionObject | null; + readonly childCog: CollectionObjectGroup | null; + readonly parentCog: CollectionObjectGroup; }; readonly toManyDependent: RR; readonly toManyIndependent: RR; @@ -6542,8 +6542,8 @@ export type CollectionObjectGroupType = { readonly tableName: 'CollectionObjectGroupType'; readonly fields: { readonly name: string; - readonly timestampcreated: string; - readonly timestampmodified: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly type: string; readonly version: number | null; }; From dbaa39040ab0db169c7b53337f670bc5c753f264 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 16 Sep 2024 16:56:32 -0400 Subject: [PATCH 048/132] Change business rules to new fieldnames --- .../components/DataModel/businessRuleDefs.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index d7697f8a896..d04de256b47 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -21,7 +21,6 @@ import type { Address, BorrowMaterial, CollectionObject, - CollectionObjectGroup, CollectionObjectGroupJoin, Determination, DNASequence, @@ -207,15 +206,15 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { CollectionObjectGroup: { fieldChecks: { - cogtype: (cog: SpecifyResource) => { + cogType: (cog): void => { // The first COJO CO will automatically have isPrimary set to True when the COG type is 'consolidated' - cog.rgetPromise('cogtype').then((cogtype) => { + cog.rgetPromise('cogType').then((cogtype) => { if (cogtype.get('type') === cogTypes.CONSOLIDATED) { - const cojos = cog.getDependentResource('cojo'); + const cojos = cog.getDependentResource('parentCojos'); // Set first CO in COG to primary cojos?.models - .find((cojo) => cojo.get('childco')) - .set('isprimary', true); + .find((cojo) => cojo.get('childCo') !== null) + ?.set('isPrimary', true); } }); }, @@ -228,13 +227,13 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { * Only a single CO in a COG can be set as primary. * When checking a CO as primary, other COs in that COG will get unchecked. */ - isprimary: (cojo: SpecifyResource) => { - if (cojo.get('isprimary') && cojo.collection !== undefined) { + isPrimary: (cojo: SpecifyResource) => { + if (cojo.get('isPrimary') && cojo.collection !== undefined) { cojo.collection.models - .filter((resource) => resource.get('childco')) + .filter((resource) => resource.get('childCo') !== null) .map((other: SpecifyResource) => { if (other.cid !== cojo.cid) { - other.set('isprimary', false); + other.set('isPrimary', false); } }); } @@ -243,13 +242,13 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { * Only a single CO in a COG can be set as substrate. * When checking a CO as substrate, other COs in that COG will get unchecked. */ - issubstrate: (cojo: SpecifyResource) => { - if (cojo.get('issubstrate') && cojo.collection !== undefined) { + isSubstrate: (cojo: SpecifyResource) => { + if (cojo.get('isSubstrate') && cojo.collection !== undefined) { cojo.collection.models - .filter((resource) => resource.get('childco')) + .filter((resource) => resource.get('childCo') !== null) .map((other: SpecifyResource) => { if (other.cid !== cojo.cid) { - other.set('issubstrate', false); + other.set('isSubstrate', false); } }); } From 44f430042ae2907b990a089d16e02c642d4c470d Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 16 Sep 2024 17:22:21 -0400 Subject: [PATCH 049/132] fix typecheck --- specifyweb/frontend/js_src/lib/components/Forms/parentTables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/parentTables.ts b/specifyweb/frontend/js_src/lib/components/Forms/parentTables.ts index 172b03af809..d7dc69bbd9d 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/parentTables.ts +++ b/specifyweb/frontend/js_src/lib/components/Forms/parentTables.ts @@ -71,7 +71,7 @@ const overrides: { CommonNameTx: 'taxon', BorrowReturnMaterial: 'borrowMaterial', CollectionObject: undefined, - CollectionObjectGroupJoin: 'parentcog', + CollectionObjectGroupJoin: 'parentCog', CollectionRelationship: undefined, Collector: 'collectingEvent', DNASequencingRun: 'dnaSequence', From cf933570fe345d1a567027279c125cd0ab8d30d6 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Tue, 17 Sep 2024 11:00:19 -0400 Subject: [PATCH 050/132] cleanup --- .../components/DataModel/businessRuleDefs.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index d04de256b47..04c1b8467bd 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -60,6 +60,21 @@ const hasNoCurrentDetermination = (collection: Collection) => determination.get('isCurrent') ); +const ensureSingleCollectionObjectCheck = ( + cojo: SpecifyResource, + field: 'isPrimary' | 'isSubstrate' +) => { + if (cojo.get(field) && cojo.collection !== undefined) { + cojo.collection.models + .filter((resource) => resource.get('childCo') !== null) + .map((other: SpecifyResource) => { + if (other.cid !== cojo.cid) { + other.set(field, false); + } + }); + } +}; + export const businessRuleDefs: MappedBusinessRuleDefs = { Address: { customInit: (address) => { @@ -228,30 +243,14 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { * When checking a CO as primary, other COs in that COG will get unchecked. */ isPrimary: (cojo: SpecifyResource) => { - if (cojo.get('isPrimary') && cojo.collection !== undefined) { - cojo.collection.models - .filter((resource) => resource.get('childCo') !== null) - .map((other: SpecifyResource) => { - if (other.cid !== cojo.cid) { - other.set('isPrimary', false); - } - }); - } + ensureSingleCollectionObjectCheck(cojo, 'isPrimary'); }, /* * Only a single CO in a COG can be set as substrate. * When checking a CO as substrate, other COs in that COG will get unchecked. */ isSubstrate: (cojo: SpecifyResource) => { - if (cojo.get('isSubstrate') && cojo.collection !== undefined) { - cojo.collection.models - .filter((resource) => resource.get('childCo') !== null) - .map((other: SpecifyResource) => { - if (other.cid !== cojo.cid) { - other.set('isSubstrate', false); - } - }); - } + ensureSingleCollectionObjectCheck(cojo, 'isSubstrate'); }, }, }, From b45a031e6130908d259aac2e065d20134a23765f Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:01:21 -0700 Subject: [PATCH 051/132] Add COG children picker to forms Fixes #5185 --- .../lib/components/FormCells/COJODialog.tsx | 42 +++++++++++++++++++ .../lib/components/FormCells/FormTable.tsx | 41 ++++++++++-------- .../components/Toolbar/QueryTablesEdit.tsx | 10 +++-- .../frontend/js_src/lib/localization/forms.ts | 3 ++ 4 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx new file mode 100644 index 00000000000..3efd38fc06a --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { useBooleanState } from '../../hooks/useBooleanState'; +import { commonText } from '../../localization/common'; +import { formsText } from '../../localization/forms'; +import { localized } from '../../utils/types'; +import { DataEntry } from '../Atoms/DataEntry'; +import { tables } from '../DataModel/tables'; +import { Dialog } from '../Molecules/Dialog'; +import { TableIcon } from '../Molecules/TableIcon'; + +export function COJODialog(): JSX.Element | null { + const [isOpen, handleOpen, handleClose] = useBooleanState(); + const COJOChildrentables = [ + tables.CollectionObject, + tables.CollectionObjectGroup, + ]; + return ( + <> + + {isOpen && ( + +
+ {COJOChildrentables.map((table) => ( +
+ + {localized(table.label)} + + +
+ ))} +
+
+ )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index e58d70ba633..9b71149ff4a 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -34,6 +34,7 @@ import { userPreferences } from '../Preferences/userPreferences'; import { SearchDialog } from '../SearchDialog'; import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; +import { COJODialog } from './COJODialog'; import { FormCell } from './index'; const cellToLabel = ( @@ -441,30 +442,34 @@ export function FormTable({ ); - const addButton = - typeof handleAddResources === 'function' && + + const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + + const addButton = isCOJO ? ( + + ) : typeof handleAddResources === 'function' && mode !== 'view' && !disableAdding && hasTablePermission( relationship.relatedTable.name, isDependent ? 'create' : 'read' ) ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - } - : (): void => - setState({ - type: 'SearchState', - }) - } - /> - ) : undefined; + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + } + : (): void => + setState({ + type: 'SearchState', + }) + } + /> + ) : undefined; return dialog === false ? ( diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx index 613975104d3..ad60e58b49a 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx @@ -42,10 +42,12 @@ export function QueryTablesEdit({ * TODO: Revert #5236 to unhide COType */ export const HIDDEN_GEO_TABLES = new Set([ - 'CollectionObjectType', - 'CollectionObjectGroup', - 'CollectionObjectGroupJoin', - 'CollectionObjectGroupType', + /* + * 'CollectionObjectType', + * 'CollectionObjectGroup', + * 'CollectionObjectGroupJoin', + * 'CollectionObjectGroupType', + */ ]); export function TablesListEdit({ isNoRestrictionMode, diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 74100c1982f..1d837f7b953 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1162,4 +1162,7 @@ export const formsText = createDictionary({ invalidTree: { 'en-us': 'Taxon does not belong to the same tree as this Object Type', }, + addCOGChildren: { + 'en-us': 'Add COG Children', + }, } as const); From 85750a793c28b786304dcbfbd2f11f6c1d707bb2 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:03:31 -0700 Subject: [PATCH 052/132] Chnage key --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 3efd38fc06a..90088a36205 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -21,7 +21,7 @@ export function COJODialog(): JSX.Element | null { {isOpen && ( From b101753ac2ebffeec3bb3c9066444f7450fda799 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:54:51 -0700 Subject: [PATCH 053/132] Add useEffect to create newResource --- .../lib/components/FormCells/COJODialog.tsx | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 90088a36205..d99548fd486 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -5,16 +5,49 @@ import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; +import type { AnySchema } from '../DataModel/helperTypes'; +import type { SpecifyResource } from '../DataModel/legacyTypes'; +import type { SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; +import type { + CollectionObject, + CollectionObjectGroup, +} from '../DataModel/types'; +import { ResourceView } from '../Forms/ResourceView'; import { Dialog } from '../Molecules/Dialog'; import { TableIcon } from '../Molecules/TableIcon'; -export function COJODialog(): JSX.Element | null { +export function COJODialog({ + parentResource, +}: { + readonly parentResource: SpecifyResource | undefined; +}): JSX.Element | null { const [isOpen, handleOpen, handleClose] = useBooleanState(); const COJOChildrentables = [ tables.CollectionObject, tables.CollectionObjectGroup, ]; + const [state, setState] = React.useState<'Add' | 'Search' | undefined>( + undefined + ); + const [resource, setResource] = React.useState< + | SpecifyTable + | SpecifyTable + | undefined + >(undefined); + const [newResource, setNewResource] = React.useState< + | SpecifyResource + | SpecifyResource + | undefined + >(undefined); + + React.useEffect(() => { + if (resource !== undefined) { + const createdResource = new resource.Resource(); + setNewResource(createdResource); + setState('Add'); + } + }, [resource]); return ( <> @@ -30,13 +63,37 @@ export function COJODialog(): JSX.Element | null {
{localized(table.label)} - + { + setResource(table); + }} + />
))}
)} + {state === 'Add' && newResource !== undefined ? ( + } + onAdd={undefined} + onClose={(): void => { + setState(undefined); + handleClose(); + }} + onDeleted={undefined} + onSaved={(): void => { + parentResource?.set('cojo', newResource as never); + setState(undefined); + handleClose(); + }} + onSaving={undefined} + /> + ) : undefined} ); } From bb463de1a718ef78a5ef9c50b3dbf3610092808e Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:10:48 -0700 Subject: [PATCH 054/132] Add search dialog --- .../lib/components/FormCells/COJODialog.tsx | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index d99548fd486..54da5edab85 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -5,7 +5,6 @@ import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; -import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import type { SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; @@ -16,6 +15,7 @@ import type { import { ResourceView } from '../Forms/ResourceView'; import { Dialog } from '../Molecules/Dialog'; import { TableIcon } from '../Molecules/TableIcon'; +import { SearchDialog } from '../SearchDialog'; export function COJODialog({ parentResource, @@ -45,7 +45,6 @@ export function COJODialog({ if (resource !== undefined) { const createdResource = new resource.Resource(); setNewResource(createdResource); - setState('Add'); } }, [resource]); return ( @@ -65,10 +64,17 @@ export function COJODialog({ {localized(table.label)} { + setState('Add'); + setResource(table); + }} + /> + { + setState('Search'); setResource(table); }} /> - ))} @@ -94,6 +100,22 @@ export function COJODialog({ onSaving={undefined} /> ) : undefined} + {state === 'Search' && resource !== undefined ? ( + } + onClose={(): void => setState(undefined)} + onSelected={([selectedResource]): void => { + // @ts-expect-error Need to refactor this to use generics + void newResource.set('cojo', selectedResource); + setState(undefined); + handleClose(); + }} + /> + ) : undefined} ); } From ae68be1dde135a76cd407bea97d4a000453aee0c Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:01:49 -0700 Subject: [PATCH 055/132] Handle add --- .../lib/components/FormCells/COJODialog.tsx | 32 +++++++++++++++---- .../lib/components/FormCells/FormTable.tsx | 12 +++++-- .../FormCells/FormTableCollection.tsx | 1 + 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 54da5edab85..567dfa4db21 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -5,8 +5,9 @@ import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; +import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; -import type { SpecifyTable } from '../DataModel/specifyTable'; +import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; import type { CollectionObject, @@ -19,8 +20,10 @@ import { SearchDialog } from '../SearchDialog'; export function COJODialog({ parentResource, + collection, }: { readonly parentResource: SpecifyResource | undefined; + readonly collection: Collection | undefined; }): JSX.Element | null { const [isOpen, handleOpen, handleClose] = useBooleanState(); const COJOChildrentables = [ @@ -80,7 +83,9 @@ export function COJODialog({ )} - {state === 'Add' && newResource !== undefined ? ( + {state === 'Add' && + newResource !== undefined && + parentResource !== undefined ? ( { - parentResource?.set('cojo', newResource as never); + const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); + const field = + newResource.specifyTable.name === 'CollectionObject' + ? 'childco' + : 'childcog'; + newCOJO.set(field, newResource as never); + newCOJO.set('parentcog', parentResource); + collection?.add(newCOJO); setState(undefined); handleClose(); }} onSaving={undefined} /> ) : undefined} - {state === 'Search' && resource !== undefined ? ( + {state === 'Search' && + resource !== undefined && + parentResource !== undefined ? ( } onClose={(): void => setState(undefined)} onSelected={([selectedResource]): void => { - // @ts-expect-error Need to refactor this to use generics - void newResource.set('cojo', selectedResource); + const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); + const field = + selectedResource.specifyTable.name === 'CollectionObject' + ? 'childco' + : 'childcog'; + newCOJO.set(field, selectedResource as never); + newCOJO.set('parentcog', parentResource); + collection?.add(newCOJO); setState(undefined); handleClose(); }} diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index 9b71149ff4a..a88a31eb8af 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -19,7 +19,8 @@ import { backboneFieldSeparator } from '../DataModel/helpers'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import type { Relationship } from '../DataModel/specifyField'; -import type { SpecifyTable } from '../DataModel/specifyTable'; +import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; +import type { CollectionObjectGroup } from '../DataModel/types'; import { FormMeta } from '../FormMeta'; import type { FormCellDefinition, SubViewSortField } from '../FormParse/cells'; import { attachmentView } from '../FormParse/webOnlyViews'; @@ -74,6 +75,7 @@ export function FormTable({ onFetchMore: handleFetchMore, isCollapsed = false, preHeaderButtons, + collection, }: { readonly relationship: Relationship; readonly isDependent: boolean; @@ -90,6 +92,7 @@ export function FormTable({ readonly onFetchMore: (() => Promise) | undefined; readonly isCollapsed: boolean | undefined; readonly preHeaderButtons?: JSX.Element; + readonly collection: Collection | undefined; }): JSX.Element { const [sortConfig, setSortConfig] = React.useState< SortConfig | undefined @@ -446,7 +449,12 @@ export function FormTable({ const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; const addButton = isCOJO ? ( - + + } + /> ) : typeof handleAddResources === 'function' && mode !== 'view' && !disableAdding && diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx index ecff42dcdf8..2b9fb8c19e7 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx @@ -68,6 +68,7 @@ export function FormTableCollection({ }} onFetchMore={collection.isComplete() ? undefined : handleFetchMore} {...props} + collection={collection} /> ); } From f3bbf598d685942c50d2cd25d9b1fe599325edb6 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:06:28 -0700 Subject: [PATCH 056/132] Typo --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 567dfa4db21..5f3bf49b4d9 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -26,7 +26,7 @@ export function COJODialog({ readonly collection: Collection | undefined; }): JSX.Element | null { const [isOpen, handleOpen, handleClose] = useBooleanState(); - const COJOChildrentables = [ + const COJOChildrenTables = [ tables.CollectionObject, tables.CollectionObjectGroup, ]; @@ -61,7 +61,7 @@ export function COJODialog({ onClose={handleClose} >
- {COJOChildrentables.map((table) => ( + {COJOChildrenTables.map((table) => (
{localized(table.label)} From 51e0d44fe8d2f3abb7d1795fd99618bbcebaaeaf Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:08:35 -0700 Subject: [PATCH 057/132] Reset resource --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 5f3bf49b4d9..8f7c1d6bd9b 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -107,6 +107,7 @@ export function COJODialog({ newCOJO.set('parentcog', parentResource); collection?.add(newCOJO); setState(undefined); + setResource(undefined); handleClose(); }} onSaving={undefined} From c94f5595064e1b966d29a1ef9ad3861a89922bcd Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:15:15 -0700 Subject: [PATCH 058/132] Unhide geo tables --- .../js_src/lib/components/SchemaConfig/Fields.tsx | 10 +--------- .../SchemaConfig/TableUniquenessRules.tsx | 4 +--- .../js_src/lib/components/SchemaConfig/Tables.tsx | 3 --- .../lib/components/Toolbar/QueryTablesEdit.tsx | 15 +-------------- .../lib/components/WbPlanView/LineComponents.tsx | 3 +-- 5 files changed, 4 insertions(+), 31 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Fields.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Fields.tsx index bbe118b1884..af860fcb76f 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Fields.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Fields.tsx @@ -66,15 +66,7 @@ export function SchemaConfigFields({ {relationships.length > 0 && ( - name !== 'collectionObjectType' - ) - : relationships - } - /> + )} diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/TableUniquenessRules.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/TableUniquenessRules.tsx index b2334201c4e..8b6d783d5ee 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/TableUniquenessRules.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/TableUniquenessRules.tsx @@ -71,9 +71,7 @@ export function TableUniquenessRules(): JSX.Element { (relationship) => (['many-to-one', 'one-to-one'] as RA).includes( relationship.type - ) && - !relationship.isVirtual && - relationship.name !== 'collectionObjectType' + ) && !relationship.isVirtual ), [table] ); diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx index 276956a0286..1edf5436416 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Tables.tsx @@ -21,7 +21,6 @@ import { Dialog } from '../Molecules/Dialog'; import { TableIcon } from '../Molecules/TableIcon'; import { hasTablePermission } from '../Permissions/helpers'; import { formatUrl } from '../Router/queryString'; -import { HIDDEN_GEO_TABLES } from '../Toolbar/QueryTablesEdit'; export function SchemaConfigTables(): JSX.Element { const { language = '' } = useParams(); @@ -127,8 +126,6 @@ export function TableList({ () => Object.values(genericTables) .filter((table) => filter(showHiddenTables, table)) - // TODO: temp fix, remove this, use to hide geo tables for COG until 9.8 release - .filter((table) => !HIDDEN_GEO_TABLES.has(table.name)) .sort(sortFunction(({ name }) => name)), [filter, showHiddenTables] ); diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx index ad60e58b49a..5e07f49b1cc 100644 --- a/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/QueryTablesEdit.tsx @@ -37,18 +37,7 @@ export function QueryTablesEdit({ /> ); } -/* - * TODO: temp fix, remove this, use to hide geo tables for COG until 9.8 release - * TODO: Revert #5236 to unhide COType - */ -export const HIDDEN_GEO_TABLES = new Set([ - /* - * 'CollectionObjectType', - * 'CollectionObjectGroup', - * 'CollectionObjectGroupJoin', - * 'CollectionObjectGroupType', - */ -]); + export function TablesListEdit({ isNoRestrictionMode, defaultTables, @@ -69,8 +58,6 @@ export function TablesListEdit({ .filter((table) => tablesFilter(isNoRestrictionMode, false, true, table, selectedValues) ) - // TODO: temp fix, remove this, use to hide geo tables for COG until 9.8 release - .filter((table) => !HIDDEN_GEO_TABLES.has(table.name)) .map(({ name, label }) => ({ name, label })); const handleChanged = (items: RA): void => diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx index 433a4761b77..d4c7648c1e0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx @@ -227,8 +227,7 @@ export function MappingElement({ fieldsData, ...props }: MappingElementProps): JSX.Element { - const { collectionObjectType, ...rest } = fieldsData; - const fieldGroups = Object.entries(rest).reduce< + const fieldGroups = Object.entries(fieldsData).reduce< R> >((fieldGroups, [fieldName, fieldData]) => { const groupName = getFieldGroupName( From 9ce312e56779ebc64c9070efe5da7a94b5ae0028 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 06:41:25 -0500 Subject: [PATCH 059/132] Define useCollection hook, reset collection on parent save, fix formtable subviews --- .../lib/components/Attachments/index.tsx | 4 +- .../lib/components/DataModel/collection.ts | 5 +- .../lib/components/DataModel/collectionApi.ts | 35 ++- .../lib/components/DataModel/resourceApi.ts | 6 +- .../lib/components/DataModel/specifyTable.ts | 5 +- .../lib/components/FormCells/FormTable.tsx | 6 +- .../FormCells/FormTableCollection.tsx | 8 +- .../FormSliders/IntegratedRecordSelector.tsx | 5 + .../RecordSelectorFromCollection.tsx | 14 +- .../js_src/lib/components/Forms/SubView.tsx | 133 ++--------- .../js_src/lib/hooks/useCollection.tsx | 220 ++++++++++++------ .../lib/hooks/useSerializedCollection.tsx | 89 +++++++ 12 files changed, 316 insertions(+), 214 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx index baef46664ac..8090f1ee5d2 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom'; import { useAsyncState, usePromise } from '../../hooks/useAsyncState'; import { useCachedState } from '../../hooks/useCachedState'; -import { useCollection } from '../../hooks/useCollection'; +import { useSerializedCollection } from '../../hooks/useSerializedCollection'; import { attachmentsText } from '../../localization/attachments'; import { commonText } from '../../localization/common'; import { schemaText } from '../../localization/schema'; @@ -125,7 +125,7 @@ function Attachments({ 'scale' ); - const [collection, setCollection, fetchMore] = useCollection( + const [collection, setCollection, fetchMore] = useSerializedCollection( React.useCallback( async (offset) => fetchCollection( diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts index f6a2e2d3e4f..e7c7d56b1cf 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts @@ -23,9 +23,10 @@ export type CollectionFetchFilters = Partial< number > > & { - readonly limit: number; + readonly limit?: number; + readonly reset?: boolean; readonly offset?: number; - readonly domainFilter: boolean; + readonly domainFilter?: boolean; readonly orderBy?: | keyof CommonFields | keyof SCHEMA['fields'] diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 2b177574c84..db9daab6164 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -6,6 +6,7 @@ import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { Backbone } from './backbone'; import type { AnySchema } from './helperTypes'; +import { DEFAULT_FETCH_LIMIT } from './collection'; import type { SpecifyResource } from './legacyTypes'; // REFACTOR: remove @ts-nocheck @@ -135,15 +136,15 @@ export const LazyCollection = Base.extend({ options ||= {}; - options.update = true; - options.remove = false; + options.update ??= true; + options.remove ??= false; options.silent = true; assert(options.at == null); options.data = options.data || _.extend({ domainfilter: this.domainfilter }, this.filters); - options.data.offset = options.offset || this.length; + options.data.offset = options.offset ?? this.length; _(options).has('limit') && (options.data.limit = options.limit); this._fetch = Backbone.Collection.prototype.fetch.call(this, options); @@ -157,6 +158,9 @@ export const LazyCollection = Base.extend({ ? this.fetch() : this; }, + getFetchOffset() { + return this.length; + }, getTotalCount() { if (_.isNumber(this._totalCount)) return Promise.resolve(this._totalCount); return this.fetchIfNotPopulated().then((_this) => _this._totalCount); @@ -236,22 +240,29 @@ export const IndependentCollection = LazyCollection.extend({ this._totalCount -= (this.removed as Set).size; - return records; + return records.filter( + ({ resource_uri }) => !(this.removed as Set).has(resource_uri) + ); }, async fetch(options) { - if (this.related.isBeingInitialized()) { + // If the related is being fetched, don't try and fetch the collection + if (this.related._fetch !== null) { return this; } this.filters[this.field.name.toLowerCase()] = this.related.id; - const offset = - this.length === 0 && this.removed.size > 0 - ? this.removed.size - : this.length; - - options = { ...(options ?? {}), silent: true, offset }; + const newOptions = { + ...options, + update: options?.reset !== true, + offset: options?.offset ?? this.getFetchOffset(), + }; - return Reflect.apply(LazyCollection.prototype.fetch, this, [options]); + return Reflect.apply(LazyCollection.prototype.fetch, this, [newOptions]); + }, + getFetchOffset() { + return this.length === 0 && this.removed.size > 0 + ? this.removed.size + : Math.floor(this.length / DEFAULT_FETCH_LIMIT) * DEFAULT_FETCH_LIMIT; }, toApiJSON(options) { const self = this; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index f45fc1340e9..ac59de6eee5 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -680,12 +680,16 @@ export const ResourceBase = Backbone.Model.extend({ if (!value) return value; // No related object // Is the related resource cached? - let toOne = this.dependentResources[fieldName]; + let toOne = + this.dependentResources[fieldName] ?? + this.independentResources[fieldName]; + if (!toOne) { _(value).isString() || softFail('expected URI, got', value); toOne = resourceFromUrl(value, { noBusinessRules: options.noBusinessRules, }); + if (field.isDependent()) { console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 1eede1b49e0..f18a0c98693 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -14,6 +14,7 @@ import { error } from '../Errors/assert'; import { attachmentView } from '../FormParse/webOnlyViews'; import { parentTableRelationship } from '../Forms/parentTables'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; +import { CollectionFetchFilters } from './collection'; import { DependentCollection, IndependentCollection, @@ -106,7 +107,9 @@ export type Collection = { toJSON>(): RA; add(resource: RA> | SpecifyResource): void; remove(resource: SpecifyResource): void; - fetch(filter?: { readonly limit: number }): Promise>; + fetch( + filters?: CollectionFetchFilters + ): Promise>; trigger(eventName: string): void; on(eventName: string, callback: (...args: RA) => void): void; once(eventName: string, callback: (...args: RA) => void): void; diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index d6173ee3495..03281b94406 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -208,7 +208,10 @@ export function FormTable({ ) : resources.length === 0 ? (

{formsText.noData()}

) : ( -
+
({ maxHeight: `${maxHeight}px`, }} viewDefinition={collapsedViewDefinition} - onScroll={handleScroll} >
[0], @@ -21,13 +22,14 @@ export function FormTableCollection({ readonly onDelete: | ((resource: SpecifyResource, index: number) => void) | undefined; + readonly onFetch?: () => void; }): JSX.Element | null { const [records, setRecords] = React.useState(Array.from(collection.models)); React.useEffect( () => resourceOn( collection, - 'add remove sort', + 'add remove sort sync', () => setRecords(Array.from(collection.models)), true ), @@ -35,9 +37,9 @@ export function FormTableCollection({ ); const handleFetchMore = React.useCallback(async () => { - await collection.fetch(); + handleFetch?.() ?? collection.fetch(); setRecords(Array.from(collection.models)); - }, [collection]); + }, [collection, handleFetch]); const isDependent = collection instanceof DependentCollection; const relationship = collection.field?.getReverse(); diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 36e62af0487..3c541dc10c3 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -9,6 +9,7 @@ import type { RA } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { DataEntry } from '../Atoms/DataEntry'; import { ReadOnlyContext } from '../Core/Contexts'; +import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection } from '../DataModel/collectionApi'; import type { AnyInteractionPreparation, @@ -43,6 +44,7 @@ export function IntegratedRecordSelector({ onClose: handleClose, onAdd: handleAdd, onDelete: handleDelete, + onFetch: handleFetch, isCollapsed: defaultCollapsed, ...rest }: Omit< @@ -54,6 +56,7 @@ export function IntegratedRecordSelector({ readonly viewName?: string; readonly urlParameter?: string; readonly onClose: () => void; + readonly onFetch?: (filters?: CollectionFetchFilters) => void; readonly sortField: SubViewSortField | undefined; }): JSX.Element { const containerRef = React.useRef(null); @@ -147,6 +150,7 @@ export function IntegratedRecordSelector({ if (isCollapsed) handleExpand(); handleDelete?.(...args); }} + onFetch={handleFetch} onSlide={(index): void => { handleExpand(); if (typeof urlParameter === 'string') setIndex(index.toString()); @@ -307,6 +311,7 @@ export function IntegratedRecordSelector({ if (isCollapsed) handleExpand(); handleDelete?.(index, 'minusButton'); }} + onFetch={handleFetch} /> ) : null} {dialogs} diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 2bc987ee9cb..0561b75bc6b 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { useTriggerState } from '../../hooks/useTriggerState'; import type { RA } from '../../utils/types'; import { defined } from '../../utils/types'; +import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection, isRelationshipCollection, @@ -13,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 { raise } from '../Errors/Crash'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { RecordSelectorProps, @@ -27,6 +27,7 @@ export function RecordSelectorFromCollection({ onAdd: handleAdd, onDelete: handleDelete, onSlide: handleSlide, + onFetch: handleFetch, children, defaultIndex = 0, ...rest @@ -45,6 +46,7 @@ export function RecordSelectorFromCollection({ readonly relationship: Relationship; readonly defaultIndex?: number; readonly children: (state: RecordSelectorState) => JSX.Element; + readonly onFetch?: (filters?: CollectionFetchFilters) => void; }): JSX.Element | null { const getRecords = React.useCallback( (): RA | undefined> => @@ -80,17 +82,17 @@ export function RecordSelectorFromCollection({ * don't need to fetch all records in between) */ if ( + typeof handleFetch === 'function' && !isToOne && isLazy && collection.related?.isNew() !== true && !collection.isComplete() && collection.models[index] === undefined ) - collection - .fetch() - .then(() => setRecords(getRecords)) - .catch(raise); - }, [collection, isLazy, getRecords, index, records.length, isToOne]); + handleFetch({ + offset: collection.getFetchOffset(), + }); + }, [collection, isLazy, index, records.length, isToOne, handleFetch]); const state = useRecordSelector({ ...rest, diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 22279228fd6..a893969763b 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; +import { useCollection } from '../../hooks/useCollection'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; import type { RA } from '../../utils/types'; -import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { DataEntry } from '../Atoms/DataEntry'; @@ -16,8 +16,6 @@ import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import { resourceOn } from '../DataModel/resource'; import type { Relationship } from '../DataModel/specifyField'; -import type { Collection } from '../DataModel/specifyTable'; -import { raise, softFail } from '../Errors/Crash'; import type { FormType } from '../FormParse'; import type { SubViewSortField } from '../FormParse/cells'; import { IntegratedRecordSelector } from '../FormSliders/IntegratedRecordSelector'; @@ -69,122 +67,36 @@ export function SubView({ }): JSX.Element { const [sortField, setSortField] = useTriggerState(initialSortField); - const fetchCollection = React.useCallback( - // If false is returned, then the Subview should not be rendered - async function fetchCollection(): Promise< - Collection | false | undefined - > { - if ( - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ) - return parentResource - .rgetCollection(relationship.name) - .then((collection) => { - // TEST: check if this can ever happen - if (collection === null || collection === undefined) - return new relationship.relatedTable.DependentCollection({ - related: parentResource, - field: relationship.getReverse(), - }) as Collection; - if (sortField === undefined) return collection; - // BUG: this does not look into related tables - const field = sortField.fieldNames[0]; - // Overwriting the tables on the collection - overwriteReadOnly( - collection, - 'models', - Array.from(collection.models).sort( - sortFunction( - (resource) => resource.get(field), - sortField.direction === 'desc' - ) - ) - ); - return collection; - }); - else { - /** - * If relationship is -to-one, create a collection for the related - * resource. This allows to reuse most of the code from the -to-many - * relationships. RecordSelector handles collections with -to-one - * related field by removing the "+" button after first record is added - * and not rendering record count or record slider. - */ - const resource = await parentResource.rgetPromise(relationship.name); - const reverse = relationship.getReverse(); - if (reverse === undefined) { - softFail( - new Error( - `Can't render a SubView for ` + - `${relationship.table.name}.${relationship.name} because ` + - `reverse relationship does not exist` - ) - ); - return false; - } - const collection = ( - relationship.isDependent() - ? new relationship.relatedTable.DependentCollection({ - related: parentResource, - field: reverse, - }) - : new relationship.relatedTable.IndependentCollection({ - related: parentResource, - field: reverse, - }) - ) as Collection; - if (relationship.isDependent() && parentResource.isNew()) - // Prevent fetching related for newly created parent - overwriteReadOnly(collection, '_totalCount', 0); - - if (typeof resource === 'object' && resource !== null) - collection.add(resource); - overwriteReadOnly( - collection, - 'related', - collection.related ?? parentResource - ); - overwriteReadOnly( - collection, - 'field', - collection.field ?? relationship.getReverse() - ); - return collection; - } - }, - [parentResource, relationship, sortField] - ); + const [collection, _setCollection, handleFetch] = useCollection({ + parentResource, + relationship, + collectionSortFunction: + sortField === undefined + ? undefined + : sortFunction( + (resource) => resource.get(sortField.fieldNames[0]), + sortField.direction === 'desc' + ), + }); - const [collection, setCollection] = React.useState< - Collection | false | undefined - >(undefined); - const versionRef = React.useRef(0); React.useEffect( () => resourceOn( parentResource, - `change:${relationship.name} saved`, + 'saved', (): void => { - versionRef.current += 1; - const localVersionRef = versionRef.current; - fetchCollection() - .then((collection) => - /* - * If value changed since begun fetching, don't update the - * collection to prevent a race condition. - * REFACTOR: simplify this - */ - versionRef.current === localVersionRef - ? setCollection(collection) - : undefined - ) - .catch(raise); + if (!relationship.isDependent()) { + handleFetch({ + offset: 0, + reset: true, + }); + } }, - true + false ), - [parentResource, relationship, fetchCollection] + [parentResource, relationship, handleFetch] ); + const subviewContext = React.useContext(SubViewContext); const [formType, setFormType] = useTriggerState(initialFormType); @@ -298,6 +210,7 @@ export function SubView({ null as never ) } + onFetch={handleFetch} /> ) : isButton ? undefined : ( diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 1e492a3f446..ebd6040a8d5 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -1,89 +1,159 @@ import React from 'react'; -import type { SerializedCollection } from '../components/DataModel/collection'; +import type { CollectionFetchFilters } from '../components/DataModel/collection'; import type { AnySchema } from '../components/DataModel/helperTypes'; -import { f } from '../utils/functools'; +import type { SpecifyResource } from '../components/DataModel/legacyTypes'; +import type { Relationship } from '../components/DataModel/specifyField'; +import type { Collection } from '../components/DataModel/specifyTable'; +import { raise } from '../components/Errors/Crash'; +import { relationshipIsToMany } from '../components/WbPlanView/mappingHelpers'; import type { GetOrSet } from '../utils/types'; -import { defined } from '../utils/types'; +import { overwriteReadOnly } from '../utils/types'; +import type { sortFunction } from '../utils/utils'; import { useAsyncState } from './useAsyncState'; -/** - * A hook for fetching a collection of resources in a paginated way - */ -export function useCollection( - fetch: (offset: number) => Promise> -): readonly [ - SerializedCollection | undefined, - GetOrSet | undefined>[1], - () => Promise +type UseCollectionProps = { + readonly parentResource: SpecifyResource; + readonly relationship: Relationship; + readonly collectionSortFunction?: ReturnType< + typeof sortFunction< + SpecifyResource, + ReturnType['get']> + > + >; +}; + +export function useCollection({ + parentResource, + relationship, + collectionSortFunction, +}: UseCollectionProps): readonly [ + ...GetOrSet | false | undefined>, + (filters?: CollectionFetchFilters) => void ] { - const fetchRef = React.useRef< - Promise | undefined> | undefined - >(undefined); + const [collection, setCollection] = useAsyncState< + Collection | false | undefined + >( + React.useCallback( + async () => + relationshipIsToMany(relationship) && + relationship.type !== 'zero-to-one' + ? fetchToManyCollection({ + parentResource, + relationship, + collectionSortFunction, + }) + : fetchToOneCollection({ + parentResource, + relationship, + collectionSortFunction, + }), + [collectionSortFunction, parentResource, relationship] + ), + false + ); - const callback = React.useCallback(async () => { - if (typeof fetchRef.current === 'object') - return fetchRef.current.then(f.undefined); - if ( - collectionRef.current !== undefined && - collectionRef.current?.records.length === - collectionRef.current?.totalCount - ) - return undefined; - fetchRef.current = fetch(collectionRef.current?.records.length ?? 0).then( - (data) => { - fetchRef.current = undefined; - return data; - } - ); - return fetchRef.current; - }, [fetch]); + const versionRef = React.useRef(0); - const currentCallback = React.useRef(f.void); + const handleFetch = React.useCallback( + (filters?: CollectionFetchFilters): void => { + if (typeof collection !== 'object') return undefined; - const [collection, setCollection] = useAsyncState( - React.useCallback(async () => { - currentCallback.current = callback; - fetchRef.current = undefined; - collectionRef.current = undefined; - return callback(); - }, [callback]), - false - ); - const collectionRef = React.useRef< - SerializedCollection | undefined - >(); - collectionRef.current = collection; + versionRef.current += 1; + const localVersionRef = versionRef.current; - const fetchMore = React.useCallback( - async () => - /* - * Ignore calls to fetchMore before collection is fetched for the first - * time - */ - currentCallback.current === callback - ? typeof fetchRef.current === 'object' - ? callback().then(f.undefined) - : callback().then((result) => - result !== undefined && - result.records.length > 0 && - // If the fetch function changed while fetching, discard the results - currentCallback.current === callback - ? setCollection((collection) => ({ - records: [ - ...defined( - collection, - 'Try to fetch more before collection is fetch.' - ).records, - ...result.records, - ], - totalCount: defined(collection).totalCount, - })) - : undefined - ) - : undefined, - [callback, collection] + collection + .fetch(filters) + .then((collection) => { + if (collection === undefined) return undefined; + /* + * If the collection is already being fetched, don't update it + * to prevent a race condition. + * REFACTOR: simplify this + */ + versionRef.current === localVersionRef + ? setCollection(collection) + : undefined; + }) + .catch(raise); + }, + [collection, setCollection] ); + return [collection, setCollection, handleFetch]; +} - return [collection, setCollection, fetchMore] as const; +const fetchToManyCollection = async ({ + parentResource, + relationship, + collectionSortFunction, +}: UseCollectionProps): Promise | undefined> => + parentResource.rgetCollection(relationship.name).then((collection) => { + // TEST: check if this can ever happen + if (collection === null || collection === undefined) + return new relationship.relatedTable.DependentCollection({ + related: parentResource, + field: relationship.getReverse(), + }) as Collection; + if (collectionSortFunction === undefined) return collection; + + // Overwriting the models on the collection + overwriteReadOnly( + collection, + 'models', + Array.from(collection.models).sort(collectionSortFunction) + ); + return collection; + }); + +async function fetchToOneCollection({ + parentResource, + relationship, + collectionSortFunction, +}: UseCollectionProps): Promise< + Collection | false | undefined +> { + /** + * If relationship is -to-one, create a collection for the related + * resource. This allows to reuse most of the code from the -to-many + * relationships. RecordSelector handles collections with -to-one + * related field by removing the "+" button after first record is added + * and not rendering record count or record slider. + */ + const resource = await parentResource.rgetPromise(relationship.name); + const reverse = relationship.getReverse(); + if (reverse === undefined) return false; + const collection = ( + relationship.isDependent() + ? new relationship.relatedTable.DependentCollection({ + related: parentResource, + field: reverse, + }) + : new relationship.relatedTable.IndependentCollection({ + related: parentResource, + field: reverse, + }) + ) as Collection; + if (relationship.isDependent() && parentResource.isNew()) + // Prevent fetching related for newly created parent + overwriteReadOnly(collection, '_totalCount', 0); + + if (typeof resource === 'object' && resource !== null) + collection.add(resource); + overwriteReadOnly( + collection, + 'related', + collection.related ?? parentResource + ); + overwriteReadOnly( + collection, + 'field', + collection.field ?? relationship.getReverse() + ); + if (collectionSortFunction !== undefined) + overwriteReadOnly( + collection, + 'models', + Array.from(collection.models).sort(collectionSortFunction) + ); + return collection; } diff --git a/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx new file mode 100644 index 00000000000..1cc18d7c584 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import type { SerializedCollection } from '../components/DataModel/collection'; +import type { AnySchema } from '../components/DataModel/helperTypes'; +import { f } from '../utils/functools'; +import type { GetOrSet } from '../utils/types'; +import { defined } from '../utils/types'; +import { useAsyncState } from './useAsyncState'; + +/** + * A hook for fetching a collection of resources in a paginated way + */ +export function useSerializedCollection( + fetch: (offset: number) => Promise> +): readonly [ + SerializedCollection | undefined, + GetOrSet | undefined>[1], + () => Promise +] { + const fetchRef = React.useRef< + Promise | undefined> | undefined + >(undefined); + + const callback = React.useCallback(async () => { + if (typeof fetchRef.current === 'object') + return fetchRef.current.then(f.undefined); + if ( + collectionRef.current !== undefined && + collectionRef.current?.records.length === + collectionRef.current?.totalCount + ) + return undefined; + fetchRef.current = fetch(collectionRef.current?.records.length ?? 0).then( + (data) => { + fetchRef.current = undefined; + return data; + } + ); + return fetchRef.current; + }, [fetch]); + + const currentCallback = React.useRef(f.void); + + const [collection, setCollection] = useAsyncState( + React.useCallback(async () => { + currentCallback.current = callback; + fetchRef.current = undefined; + collectionRef.current = undefined; + return callback(); + }, [callback]), + false + ); + const collectionRef = React.useRef< + SerializedCollection | undefined + >(); + collectionRef.current = collection; + + const fetchMore = React.useCallback( + async () => + /* + * Ignore calls to fetchMore before collection is fetched for the first + * time + */ + currentCallback.current === callback + ? typeof fetchRef.current === 'object' + ? callback().then(f.undefined) + : callback().then((result) => + result !== undefined && + result.records.length > 0 && + // If the fetch function changed while fetching, discard the results + currentCallback.current === callback + ? setCollection((collection) => ({ + records: [ + ...defined( + collection, + 'Try to fetch more before collection is fetch.' + ).records, + ...result.records, + ], + totalCount: defined(collection).totalCount, + })) + : undefined + ) + : undefined, + [callback, collection] + ); + + return [collection, setCollection, fetchMore] as const; +} From 134c65fde55192925d264b920d5c012f286a60ec Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 06:55:58 -0500 Subject: [PATCH 060/132] Resolve TS errors --- .../js_src/lib/components/DataModel/collectionApi.ts | 3 +++ .../js_src/lib/components/DataModel/specifyTable.ts | 1 + specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx | 3 ++- specifyweb/frontend/js_src/lib/hooks/useCollection.tsx | 6 +++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index db9daab6164..14d5d680f07 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -88,6 +88,9 @@ export const DependentCollection = Base.extend({ isComplete() { return true; }, + getFetchOffset() { + return 0; + }, fetch: fakeFetch, sync: notSupported, create: notSupported, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index f18a0c98693..4995661d1a6 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -102,6 +102,7 @@ export type Collection = { /* eslint-disable @typescript-eslint/method-signature-style */ isComplete(): boolean; getTotalCount(): Promise; + getFetchOffset(): number; indexOf(resource: SpecifyResource): number; // eslint-disable-next-line @typescript-eslint/naming-convention toJSON>(): RA; diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index a893969763b..5c04c60475c 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -12,6 +12,7 @@ import { DataEntry } from '../Atoms/DataEntry'; import { attachmentSettingsPromise } from '../Attachments/attachments'; import { attachmentRelatedTables } from '../Attachments/utils'; import { ReadOnlyContext } from '../Core/Contexts'; +import type { CollectionFetchFilters } from '../DataModel/collection'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import { resourceOn } from '../DataModel/resource'; @@ -89,7 +90,7 @@ export function SubView({ handleFetch({ offset: 0, reset: true, - }); + } as CollectionFetchFilters); } }, false diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index ebd6040a8d5..f9fd04703a6 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -63,7 +63,7 @@ export function useCollection({ const localVersionRef = versionRef.current; collection - .fetch(filters) + .fetch(filters as CollectionFetchFilters) .then((collection) => { if (collection === undefined) return undefined; /* @@ -71,8 +71,8 @@ export function useCollection({ * to prevent a race condition. * REFACTOR: simplify this */ - versionRef.current === localVersionRef - ? setCollection(collection) + return versionRef.current === localVersionRef + ? void setCollection(collection) : undefined; }) .catch(raise); From dd61235de05fecf4e5694833474a07e2e7b1e78c Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 07:21:11 -0500 Subject: [PATCH 061/132] Resolve backend tests, update URI for frontend format test --- .../lib/components/DataModel/collectionApi.ts | 2 +- specifyweb/specify/tests/test_api.py | 24 ++++--------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 14d5d680f07..01f3dd18884 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -113,7 +113,7 @@ export const LazyCollection = Base.extend({ return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; }, isComplete() { - return this._neverFetched && this.length === this._totalCount; + return !this._neverFetched && this.length === this._totalCount; }, parse(resp) { let objects; diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index 55e6ef677a9..9480581cff1 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -535,7 +535,9 @@ def test_indepenent_to_many_removing_from_inline(self): self.assertEqual(accession, self.collectionobjects[0].accession) - collection_objects_to_set = [self.collectionobjects[0], self.collectionobjects[3]] + collection_objects_to_remove = [self.collectionobjects[0], self.collectionobjects[3]] + + cos_to_keep = [collection_object for collection_object in self.collectionobjects if not collection_object in collection_objects_to_remove] accession_data = { 'accessionnumber': "a", @@ -543,14 +545,13 @@ def test_indepenent_to_many_removing_from_inline(self): 'collectionobjects': { "remove": [ api.uri_for_model('collectionobject', collection_object.id) - for index, collection_object in enumerate(collection_objects_to_set) - if index % 2 == 0 + for collection_object in collection_objects_to_remove ] } } accession = api.update_obj(self.collection, self.agent, 'Accession', accession.id, accession.version, accession_data) - self.assertEqual(list(accession.collectionobjects.all()), collection_objects_to_set) + self.assertEqual(list(accession.collectionobjects.all()), cos_to_keep) # ensure the other CollectionObjects have not been deleted self.assertEqual(len(models.Collectionobject.objects.all()), len(self.collectionobjects)) @@ -657,21 +658,6 @@ def test_reassigning_independent_to_many(self): self.assertEqual(self.collectionobjects[0].accession, acc2) self.assertEqual(self.collectionobjects[1].accession, acc2) - def test_inline_error_handling(self): - collection_object_data = { - 'id': self.collectionobjects[0].id, - 'catalognumber': self.collectionobjects[0].catalognumber, - 'collection': api.uri_for_model('Collection', self.collection.id), - 'determinations': f'/api/specify/determination/?collectionobject={self.collectionobjects[0].id}' - } - - with self.assertRaises(AssertionError): - api.update_obj(self.collection, self.agent, - 'Collectionobject', self.collectionobjects[0].id, - self.collectionobjects[0].version, collection_object_data) - - - # version control on inlined resources should be tested From d512e333358f649de9c67bfd1b7b4efa3a1c87b8 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 08:03:10 -0500 Subject: [PATCH 062/132] Change ordering for setting of _neverFetched --- .../js_src/lib/components/DataModel/collectionApi.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 01f3dd18884..870bc7cfdf0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -102,7 +102,6 @@ export const LazyCollection = Base.extend({ constructor(options = {}) { this.table = this.model; Base.call(this, null, options); - this._neverFetched = true; this._totalCount = undefined; this.filters = options.filters || {}; this.domainfilter = @@ -129,11 +128,11 @@ export const LazyCollection = Base.extend({ return objects; }, async fetch(options) { - this._neverFetched = false; - if (this._fetch) return this._fetch; else if (this.isComplete() || this.related?.isNew()) return this; + this._neverFetched = false; + if (this.isComplete()) console.error('fetching for already filled collection'); @@ -249,9 +248,8 @@ export const IndependentCollection = LazyCollection.extend({ }, async fetch(options) { // If the related is being fetched, don't try and fetch the collection - if (this.related._fetch !== null) { - return this; - } + if (this.related._fetch !== null) return this; + this.filters[this.field.name.toLowerCase()] = this.related.id; const newOptions = { From 473ea5c41f91cb304fed729133d227afa5ddd2d6 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 09:16:46 -0500 Subject: [PATCH 063/132] Improve orderby resolution for to-many subviews --- .../lib/components/DataModel/collectionApi.ts | 1 + .../js_src/lib/components/Forms/SubView.tsx | 9 +--- .../js_src/lib/hooks/useCollection.tsx | 46 ++++++++++++------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 870bc7cfdf0..2d68b97eada 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -147,6 +147,7 @@ export const LazyCollection = Base.extend({ options.data || _.extend({ domainfilter: this.domainfilter }, this.filters); options.data.offset = options.offset ?? this.length; + options.data.orderby = options.orderby; _(options).has('limit') && (options.data.limit = options.limit); this._fetch = Backbone.Collection.prototype.fetch.call(this, options); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 5c04c60475c..54b674005e4 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -6,7 +6,6 @@ import { useCollection } from '../../hooks/useCollection'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; import type { RA } from '../../utils/types'; -import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { DataEntry } from '../Atoms/DataEntry'; import { attachmentSettingsPromise } from '../Attachments/attachments'; @@ -71,13 +70,7 @@ export function SubView({ const [collection, _setCollection, handleFetch] = useCollection({ parentResource, relationship, - collectionSortFunction: - sortField === undefined - ? undefined - : sortFunction( - (resource) => resource.get(sortField.fieldNames[0]), - sortField.direction === 'desc' - ), + sortBy: sortField, }); React.useEffect( diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index f9fd04703a6..0e41415580f 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -6,27 +6,23 @@ import type { SpecifyResource } from '../components/DataModel/legacyTypes'; import type { Relationship } from '../components/DataModel/specifyField'; import type { Collection } from '../components/DataModel/specifyTable'; import { raise } from '../components/Errors/Crash'; +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 type { sortFunction } from '../utils/utils'; +import { sortFunction } from '../utils/utils'; import { useAsyncState } from './useAsyncState'; type UseCollectionProps = { readonly parentResource: SpecifyResource; readonly relationship: Relationship; - readonly collectionSortFunction?: ReturnType< - typeof sortFunction< - SpecifyResource, - ReturnType['get']> - > - >; + readonly sortBy?: SubViewSortField; }; export function useCollection({ parentResource, relationship, - collectionSortFunction, + sortBy, }: UseCollectionProps): readonly [ ...GetOrSet | false | undefined>, (filters?: CollectionFetchFilters) => void @@ -41,14 +37,14 @@ export function useCollection({ ? fetchToManyCollection({ parentResource, relationship, - collectionSortFunction, + sortBy, }) : fetchToOneCollection({ parentResource, relationship, - collectionSortFunction, + sortBy, }), - [collectionSortFunction, parentResource, relationship] + [sortBy, parentResource, relationship] ), false ); @@ -85,7 +81,7 @@ export function useCollection({ const fetchToManyCollection = async ({ parentResource, relationship, - collectionSortFunction, + sortBy, }: UseCollectionProps): Promise | undefined> => parentResource.rgetCollection(relationship.name).then((collection) => { // TEST: check if this can ever happen @@ -94,13 +90,21 @@ const fetchToManyCollection = async ({ related: parentResource, field: relationship.getReverse(), }) as Collection; - if (collectionSortFunction === undefined) return collection; + if (sortBy === undefined) return collection; + + // BUG: this does not look into related tables + const field = sortBy.fieldNames[0]; // Overwriting the models on the collection overwriteReadOnly( collection, 'models', - Array.from(collection.models).sort(collectionSortFunction) + Array.from(collection.models).sort( + sortFunction( + (resource) => resource.get(field), + sortBy.direction === 'desc' + ) + ) ); return collection; }); @@ -108,7 +112,7 @@ const fetchToManyCollection = async ({ async function fetchToOneCollection({ parentResource, relationship, - collectionSortFunction, + sortBy, }: UseCollectionProps): Promise< Collection | false | undefined > { @@ -149,11 +153,19 @@ async function fetchToOneCollection({ 'field', collection.field ?? relationship.getReverse() ); - if (collectionSortFunction !== undefined) + if (sortBy !== undefined) { + // BUG: this does not look into related tables + const field = sortBy.fieldNames[0]; overwriteReadOnly( collection, 'models', - Array.from(collection.models).sort(collectionSortFunction) + Array.from(collection.models).sort( + sortFunction( + (resource) => resource.get(field), + sortBy.direction === 'desc' + ) + ) ); + } return collection; } From cf84978f7b31ecd612c60360eead14b594262b6b Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 18 Sep 2024 09:37:54 -0500 Subject: [PATCH 064/132] Resolving failing frontend test --- .../frontend/js_src/lib/components/DataModel/collectionApi.ts | 4 ++-- .../lib/components/Formatters/__tests__/formatters.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 2d68b97eada..aaf3a8b9828 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -131,11 +131,11 @@ export const LazyCollection = Base.extend({ if (this._fetch) return this._fetch; else if (this.isComplete() || this.related?.isNew()) return this; - this._neverFetched = false; - if (this.isComplete()) console.error('fetching for already filled collection'); + this._neverFetched = false; + options ||= {}; options.update ??= true; diff --git a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts index 9b2f8fbd7df..ee14ccd2fc3 100644 --- a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/formatters.test.ts @@ -120,7 +120,7 @@ const taxonCitation = { referenceWork: getResourceApiUrl('ReferenceWork', referenceWorkId), }; overrideAjax( - '/api/specify/taxoncitation/?referencework=1&domainfilter=false&limit=0', + '/api/specify/taxoncitation/?domainfilter=false&referencework=1&offset=0', { meta: { total_count: 1, From 2658814a6a389f7c8e97900a2c357800958b7625 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 20 Sep 2024 15:22:41 -0400 Subject: [PATCH 065/132] Create utils for business rules --- .../components/DataModel/businessRuleDefs.ts | 34 ++++------------ .../components/DataModel/businessRuleUtils.ts | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 04c1b8467bd..1b784702712 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -2,6 +2,12 @@ import { formsText } from '../../localization/forms'; import { resourcesText } from '../../localization/resources'; import { f } from '../../utils/functools'; import type { BusinessRuleResult } from './businessRules'; +import { + CURRENT_DETERMINATION_KEY, + DETERMINATION_TAXON_KEY, + ensureSingleCollectionObjectCheck, + hasNoCurrentDetermination, +} from './businessRuleUtils'; import { cogTypes } from './helpers'; import type { AnySchema, TableFields } from './helperTypes'; import { @@ -51,30 +57,6 @@ type MappedBusinessRuleDefs = { readonly [TABLE in keyof Tables]?: BusinessRuleDefs; }; -const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; -const DETERMINATION_TAXON_KEY = 'determination-taxon'; - -const hasNoCurrentDetermination = (collection: Collection) => - collection.models.length > 0 && - !collection.models.some((determination: SpecifyResource) => - determination.get('isCurrent') - ); - -const ensureSingleCollectionObjectCheck = ( - cojo: SpecifyResource, - field: 'isPrimary' | 'isSubstrate' -) => { - if (cojo.get(field) && cojo.collection !== undefined) { - cojo.collection.models - .filter((resource) => resource.get('childCo') !== null) - .map((other: SpecifyResource) => { - if (other.cid !== cojo.cid) { - other.set(field, false); - } - }); - } -}; - export const businessRuleDefs: MappedBusinessRuleDefs = { Address: { customInit: (address) => { @@ -242,14 +224,14 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { * Only a single CO in a COG can be set as primary. * When checking a CO as primary, other COs in that COG will get unchecked. */ - isPrimary: (cojo: SpecifyResource) => { + isPrimary: (cojo: SpecifyResource): void => { ensureSingleCollectionObjectCheck(cojo, 'isPrimary'); }, /* * Only a single CO in a COG can be set as substrate. * When checking a CO as substrate, other COs in that COG will get unchecked. */ - isSubstrate: (cojo: SpecifyResource) => { + isSubstrate: (cojo: SpecifyResource): void => { ensureSingleCollectionObjectCheck(cojo, 'isSubstrate'); }, }, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts new file mode 100644 index 00000000000..a6c8f33125a --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts @@ -0,0 +1,40 @@ +import { SpecifyResource } from './legacyTypes'; +import { Collection } from './specifyTable'; +import { CollectionObjectGroupJoin, Determination } from './types'; + +// Save blocker keys used in businessRuleDefs.ts +export const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; +export const DETERMINATION_TAXON_KEY = 'determination-taxon'; + +/** + * + * Calculates whether a collection of determinations has any current determinations or not + * Used in CO -> Determination -> isCurrent business rule + */ +export const hasNoCurrentDetermination = ( + collection: Collection +) => + collection.models.length > 0 && + !collection.models.some((determination: SpecifyResource) => + determination.get('isCurrent') + ); + +/** + * + * Ensures only one CO in a COG can be checked as isPrimary or isSubstrate + * Used in COG business rules: https://github.com/specify/specify7/issues/5246 + */ +export const ensureSingleCollectionObjectCheck = ( + cojo: SpecifyResource, + field: 'isPrimary' | 'isSubstrate' +) => { + if (cojo.get(field) && cojo.collection !== undefined) { + cojo.collection.models + .filter((resource) => resource.get('childCo') !== null) + .map((other: SpecifyResource) => { + if (other.cid !== cojo.cid) { + other.set(field, false); + } + }); + } +}; From 58a257391e977643db58f0a21a37089f9b020ed0 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 20 Sep 2024 15:23:02 -0400 Subject: [PATCH 066/132] add cog business rule test --- .../DataModel/__tests__/businessRules.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 2441c7431cb..adf5585ff59 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -210,6 +210,30 @@ describe('Collection Object business rules', () => { }); }); +describe('CollectionObjectGroup business rules', () => { + test('Only one CO COJO can be primary', () => { + const cog = new tables.CollectionObjectGroup.Resource({ + id: 1, + cogType: getResourceApiUrl('CollectionObjectGroupType', 1), + }); + + const cojo1 = new tables.CollectionObjectGroupJoin.Resource({ + isPrimary: true, + childCo: getResourceApiUrl('CollectionObject', 1), + parentCog: getResourceApiUrl('CollectionObjectGroup', 1), + }); + const cojo2 = new tables.CollectionObjectGroupJoin.Resource({ + isPrimary: false, + childCo: getResourceApiUrl('CollectionObject', 2), + parentCog: getResourceApiUrl('CollectionObjectGroup', 1), + }); + cog.set('parentCojos', [cojo1, cojo2]); + cojo2.set('isPrimary', true); + + expect(cojo1.get('isPrimary')).toBe(false); + }); +}); + describe('DNASequence business rules', () => { test('fieldCheck geneSequence', async () => { const dNASequence = new tables.DNASequence.Resource({ From 33515c4c719c9e08c85b27c007390b4f613ff1d9 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 24 Sep 2024 14:53:37 -0500 Subject: [PATCH 067/132] Prevent storing independent toMany if fetch not successful --- .../js_src/lib/components/DataModel/resourceApi.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index ac59de6eee5..528dc184a2d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -338,7 +338,7 @@ export const ResourceBase = Backbone.Model.extend({ } default: { throw new Error( - `setDependentToOne: unhandled field type: ${field.type}` + `storeIndependentToOne: unhandled field type: ${field.type} for ${this.specifyTable.name}.${field.name}` ); } } @@ -810,9 +810,11 @@ export const ResourceBase = Backbone.Model.extend({ ? new relatedTable.IndependentCollection(collectionOptions) : existingToMany; - return collection.fetch().then((fetchedCollection) => { - this.storeIndependent(field, fetchedCollection); - return fetchedCollection; + return collection.fetch({ + // Only store the collection if fetch is successful (doesn't return undefined) + success: (collection) => { + this.storeIndependent(field, collection); + }, }); }, async save({ From e9f30e374bb5b34313dca320fd0e842d14a9cbd6 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Tue, 24 Sep 2024 16:15:00 -0400 Subject: [PATCH 068/132] Add empty check for value conditions --- .../frontend/js_src/lib/components/FormCells/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx index b228daff8ed..df751482253 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx @@ -235,7 +235,10 @@ const cellRenderers: { break; } const value = await fetchPathAsString(resource, condition.field); - if (!destructorCalled && value === condition.value) { + if ( + (!destructorCalled && value === condition.value) || + (condition.value === 'EMPTY' && value === '') + ) { foundIndex = Number.parseInt(index); break; } From 719c01ae860adde897f8406c6a40a2b2d67ce85b Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 25 Sep 2024 10:03:10 -0500 Subject: [PATCH 069/132] Regenerate datamodel test file --- .../tests/ajax/static/context/datamodel.json | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json index a4b3542b624..dd0438cbf64 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json @@ -29588,7 +29588,7 @@ "classname": "edu.ku.brc.specify.datamodel.SpAuditLog", "table": "spauditlog", "tableId": 530, - "system": true, + "system": false, "idColumn": "SpAuditLogID", "idFieldName": "spAuditLogId", "fields": [ @@ -38130,7 +38130,7 @@ "type": "java.lang.Integer" }, { - "name": "timestampcreated", + "name": "timestampCreated", "column": "TimestampCreated", "indexed": false, "unique": false, @@ -38138,7 +38138,7 @@ "type": "java.sql.Timestamp" }, { - "name": "timestampmodified", + "name": "timestampModified", "column": "TimestampModified", "indexed": false, "unique": false, @@ -38261,7 +38261,7 @@ "type": "java.lang.Integer" }, { - "name": "timestampcreated", + "name": "timestampCreated", "column": "TimestampCreated", "indexed": false, "unique": false, @@ -38269,7 +38269,7 @@ "type": "java.sql.Timestamp" }, { - "name": "timestampmodified", + "name": "timestampModified", "column": "TimestampModified", "indexed": false, "unique": false, @@ -38386,7 +38386,7 @@ "column": "CollectionID" }, { - "name": "cogtype", + "name": "cogType", "type": "many-to-one", "required": true, "dependent": false, @@ -38394,12 +38394,12 @@ "column": "COGTypeID" }, { - "name": "parentcojos", + "name": "parentCojos", "type": "one-to-many", "required": false, "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", - "otherSideName": "parentcog" + "otherSideName": "parentCog" }, { "name": "cojo", @@ -38407,7 +38407,7 @@ "required": false, "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", - "otherSideName": "childcog" + "otherSideName": "childCog" }, { "name": "createdByAgent", @@ -38437,7 +38437,7 @@ "idFieldName": "collectionObjectGroupJoinId", "fields": [ { - "name": "isprimary", + "name": "isPrimary", "column": "IsPrimary", "indexed": false, "unique": false, @@ -38445,7 +38445,7 @@ "type": "java.lang.Boolean" }, { - "name": "issubstrate", + "name": "isSubstrate", "column": "IsSubstrate", "indexed": false, "unique": false, @@ -38469,7 +38469,7 @@ "type": "java.lang.Integer" }, { - "name": "timestampcreated", + "name": "timestampCreated", "column": "TimestampCreated", "indexed": false, "unique": false, @@ -38477,7 +38477,7 @@ "type": "java.sql.Timestamp" }, { - "name": "timestampmodified", + "name": "timestampModified", "column": "TimestampModified", "indexed": false, "unique": false, @@ -38562,7 +38562,7 @@ ], "relationships": [ { - "name": "parentcog", + "name": "parentCog", "type": "many-to-one", "required": true, "dependent": false, @@ -38571,8 +38571,8 @@ "otherSideName": "parentcojos" }, { - "name": "childcog", - "type": "many-to-one", + "name": "childCog", + "type": "one-to-one", "required": false, "dependent": false, "relatedModelName": "CollectionObjectGroup", @@ -38580,8 +38580,8 @@ "otherSideName": "cojo" }, { - "name": "childco", - "type": "many-to-one", + "name": "childCo", + "type": "one-to-one", "required": false, "dependent": false, "relatedModelName": "CollectionObject", @@ -38626,7 +38626,7 @@ "type": "java.lang.Integer" }, { - "name": "timestampcreated", + "name": "timestampCreated", "column": "TimestampCreated", "indexed": false, "unique": false, @@ -38634,7 +38634,7 @@ "type": "java.sql.Timestamp" }, { - "name": "timestampmodified", + "name": "timestampModified", "column": "TimestampModified", "indexed": false, "unique": false, From 3a1224c6f5273606b021d13d94649726aa6540b0 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 25 Sep 2024 10:32:37 -0500 Subject: [PATCH 070/132] Update tests to be inline with updated datamodel test file --- .../DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap | 2 +- .../js_src/lib/components/DataModel/businessRuleUtils.ts | 2 +- .../FormEditor/__tests__/__snapshots__/createView.test.ts.snap | 1 + .../js_src/lib/components/Forms/__tests__/parentTables.test.tsx | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap index 38b0269a18c..530e2b6d3e1 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap @@ -1293,7 +1293,7 @@ exports[`tableScoping 1`] = ` "CollectionObjectAttribute": undefined, "CollectionObjectCitation": "collectionObject", "CollectionObjectGroup": "collection", - "CollectionObjectGroupJoin": "parentcog > collection", + "CollectionObjectGroupJoin": "parentCog > collection", "CollectionObjectGroupType": "collection", "CollectionObjectProperty": "collectionObject", "CollectionObjectType": "collection", diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts index a6c8f33125a..f613db0ec31 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts @@ -31,7 +31,7 @@ export const ensureSingleCollectionObjectCheck = ( if (cojo.get(field) && cojo.collection !== undefined) { cojo.collection.models .filter((resource) => resource.get('childCo') !== null) - .map((other: SpecifyResource) => { + .forEach((other: SpecifyResource) => { if (other.cid !== cojo.cid) { other.set(field, false); } diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap index e1d9258ce0c..8d11c38c162 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap @@ -127,6 +127,7 @@ exports[`Tables with form tables computed correctly 1`] = ` "[table RepositoryAgreement]", "[table RepositoryAgreementAttachment]", "[table Shipment]", + "[table SpAuditLog]", "[table Storage]", "[table StorageAttachment]", "[table StorageTreeDef]", diff --git a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx index 455b91b22b9..66f10effad8 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx @@ -31,7 +31,7 @@ test('Parent table relationships are calculated properly', () => "CollectionObjectAttachment": "[relationship CollectionObjectAttachment.collectionObject]", "CollectionObjectAttr": "[relationship CollectionObjectAttr.collectionObject]", "CollectionObjectCitation": "[relationship CollectionObjectCitation.collectionObject]", - "CollectionObjectGroupJoin": "[relationship CollectionObjectGroupJoin.parentcog]", + "CollectionObjectGroupJoin": "[relationship CollectionObjectGroupJoin.parentCog]", "CollectionObjectProperty": "[relationship CollectionObjectProperty.collectionObject]", "Collector": "[relationship Collector.collectingEvent]", "CommonNameTx": "[relationship CommonNameTx.taxon]", From 836b4c2618d46317b8dcbe9d6a3cf16eea6ee000 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 25 Sep 2024 15:37:01 +0000 Subject: [PATCH 071/132] Lint code with ESLint and Prettier Triggered by 3a1224c6f5273606b021d13d94649726aa6540b0 on branch refs/heads/issue-5246 --- .../js_src/lib/components/DataModel/businessRuleUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts index f613db0ec31..f0a811a1e7e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts @@ -1,6 +1,6 @@ -import { SpecifyResource } from './legacyTypes'; -import { Collection } from './specifyTable'; -import { CollectionObjectGroupJoin, Determination } from './types'; +import type { SpecifyResource } from './legacyTypes'; +import type { Collection } from './specifyTable'; +import type { CollectionObjectGroupJoin, Determination } from './types'; // Save blocker keys used in businessRuleDefs.ts export const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; From 6d680a18d5b76fa1af4c1877c6ee29ef46e9e2c0 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 25 Sep 2024 14:34:25 -0400 Subject: [PATCH 072/132] Add test case for substrate --- .../DataModel/__tests__/businessRules.test.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index adf5585ff59..2e7ec80be95 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -211,26 +211,44 @@ describe('Collection Object business rules', () => { }); describe('CollectionObjectGroup business rules', () => { - test('Only one CO COJO can be primary', () => { + const getBaseCOG = () => { const cog = new tables.CollectionObjectGroup.Resource({ id: 1, cogType: getResourceApiUrl('CollectionObjectGroupType', 1), + resource_uri: getResourceApiUrl('CollectionObjectGroup', 1), }); const cojo1 = new tables.CollectionObjectGroupJoin.Resource({ - isPrimary: true, + isPrimary: false, + isSubstrate: true, childCo: getResourceApiUrl('CollectionObject', 1), parentCog: getResourceApiUrl('CollectionObjectGroup', 1), }); const cojo2 = new tables.CollectionObjectGroupJoin.Resource({ - isPrimary: false, + isPrimary: true, + isSubstrate: false, childCo: getResourceApiUrl('CollectionObject', 2), parentCog: getResourceApiUrl('CollectionObjectGroup', 1), }); + cog.set('parentCojos', [cojo1, cojo2]); - cojo2.set('isPrimary', true); + return { cog, cojo1, cojo2 }; + }; + + test('Only one CO COJO can be primary', () => { + const { cojo1, cojo2 } = getBaseCOG(); + cojo1.set('isPrimary', true); + + expect(cojo1.get('isPrimary')).toBe(true); + expect(cojo2.get('isPrimary')).toBe(false); + }); + + test('Only one CO COJO can be substrate', () => { + const { cojo1, cojo2 } = getBaseCOG(); + cojo2.set('isSubstrate', true); - expect(cojo1.get('isPrimary')).toBe(false); + expect(cojo1.get('isSubstrate')).toBe(false); + expect(cojo2.get('isSubstrate')).toBe(true); }); }); From be03bcd80fa2a8196919262a11237636894ae6c7 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Fri, 27 Sep 2024 10:36:43 -0500 Subject: [PATCH 073/132] Set independent toOne on resource change --- .../lib/components/DataModel/collection.ts | 2 ++ .../lib/components/DataModel/collectionApi.ts | 12 ++++++++-- .../lib/components/DataModel/resourceApi.ts | 3 ++- .../js_src/lib/hooks/useCollection.tsx | 24 +++++++++---------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts index e7c7d56b1cf..0c5b516245d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts @@ -10,6 +10,7 @@ import type { } from './helperTypes'; import { parseResourceUrl } from './resource'; import { serializeResource } from './serializers'; +import { Collection } from './specifyTable'; import { genericTables, tables } from './tables'; import type { Tables } from './types'; @@ -32,6 +33,7 @@ export type CollectionFetchFilters = Partial< | keyof SCHEMA['fields'] | `-${string & keyof CommonFields}` | `-${string & keyof SCHEMA['fields']}`; + readonly success?: (collection: Collection) => void; }; export const DEFAULT_FETCH_LIMIT = 20; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index aaf3a8b9828..b21230c03e1 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -8,6 +8,7 @@ import { Backbone } from './backbone'; import type { AnySchema } from './helperTypes'; import { DEFAULT_FETCH_LIMIT } from './collection'; import type { SpecifyResource } from './legacyTypes'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; // REFACTOR: remove @ts-nocheck @@ -187,10 +188,16 @@ export const IndependentCollection = LazyCollection.extend({ this.updated = {}; }, initialize(_tables, options) { + setupToOne(this, options); + this.on( 'change', function (resource: SpecifyResource) { if (!resource.isBeingInitialized()) { + if (relationshipIsToMany(this.field)) { + const otherSideName = this.field.getReverse().name; + this.related.set(otherSideName, resource); + } this.updated[resource.cid] = resource; this.trigger('saverequired'); } @@ -230,8 +237,9 @@ export const IndependentCollection = LazyCollection.extend({ this.updated = {}; this.removed = new Set(); }); - - setupToOne(this, options); + }, + isComplete() { + return false; }, parse(resp) { const self = this; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 528dc184a2d..584adc1cad3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -7,6 +7,7 @@ import { Http } from '../../utils/ajax/definitions'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; import { isRelationshipCollection } from './collectionApi'; @@ -301,7 +302,7 @@ export const ResourceBase = Backbone.Model.extend({ ) { assert(!field.isDependent()); - if (field.type === 'one-to-many') + if (relationshipIsToMany(field)) this._storeIndependentToMany(field, related); else this._storeIndependentToOne(field, related); }, diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 0e41415580f..638d4ad55df 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -59,18 +59,18 @@ export function useCollection({ const localVersionRef = versionRef.current; collection - .fetch(filters as CollectionFetchFilters) - .then((collection) => { - if (collection === undefined) return undefined; - /* - * If the collection is already being fetched, don't update it - * to prevent a race condition. - * REFACTOR: simplify this - */ - return versionRef.current === localVersionRef - ? void setCollection(collection) - : undefined; - }) + .fetch({ + ...filters, + success: (collection) => { + /* + * If the collection is already being fetched, don't update it + * to prevent a race condition. + * REFACTOR: simplify this + */ + if (versionRef.current === localVersionRef) + setCollection(collection); + }, + } as CollectionFetchFilters) .catch(raise); }, [collection, setCollection] From 7bcab20eb807a627fc345c944192c4bee64d2a50 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 30 Sep 2024 10:11:18 -0500 Subject: [PATCH 074/132] Fetch independnet toOne when rgetPromise called --- .../DataModel/__tests__/resourceApi.test.ts | 28 ++++++++++++++++--- .../lib/components/DataModel/resourceApi.ts | 3 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 57c4118a250..e56274cdeeb 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -21,7 +21,11 @@ const collectionObjectUrl = getResourceApiUrl( ); const accessionId = 11; const accessionUrl = getResourceApiUrl('Accession', accessionId); -const collectingEventUrl = getResourceApiUrl('CollectingEvent', 8868); +const collectingEventId = 8868; +const collectingEventUrl = getResourceApiUrl( + 'CollectingEvent', + collectingEventId +); const determinationUrl = getResourceApiUrl('Determination', 123); const determinationsResponse: RA>> = [ @@ -64,9 +68,12 @@ const accessionResponse = { }; overrideAjax(accessionUrl, accessionResponse); +const collectingEventText = 'testCollectingEvent'; + const collectingEventResponse = { resource_uri: collectingEventUrl, - id: 8868, + text1: collectingEventText, + id: collectingEventId, }; overrideAjax(collectingEventUrl, collectingEventResponse); @@ -172,14 +179,27 @@ describe('rgetCollection', () => { expect(agents.models).toHaveLength(0); }); - test('repeated calls for independent return different object', async () => { + test('repeated calls for independent return same object', async () => { const resource = new tables.CollectionObject.Resource({ id: collectionObjectId, }); const firstCollectingEvent = await resource.rgetPromise('collectingEvent'); const secondCollectingEvent = await resource.rgetPromise('collectingEvent'); expect(firstCollectingEvent?.toJSON()).toEqual(collectingEventResponse); - expect(firstCollectingEvent).not.toBe(secondCollectingEvent); + expect(firstCollectingEvent).toBe(secondCollectingEvent); + }); + + test('call for independent refetches related', async () => { + const resource = new tables.CollectionObject.Resource({ + id: collectionObjectId, + }); + const newCollectingEvent = new tables.CollectingEvent.Resource({ + id: collectingEventId, + text1: 'someOtherText', + }); + resource.set('collectingEvent', newCollectingEvent); + const firstCollectingEvent = await resource.rgetPromise('collectingEvent'); + expect(firstCollectingEvent?.get('text1')).toEqual(collectingEventText); }); test('repeated calls for dependent return same object', async () => { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 584adc1cad3..1bac466e73e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -695,7 +695,8 @@ export const ResourceBase = Backbone.Model.extend({ console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); } else { - this.storeIndependent(field, toOne); + const fetchedToOne = toOne.isNew() ? toOne : await toOne.fetch(); + this.storeIndependent(field, fetchedToOne); } } // If we want a field within the related resource then recur From 1a81c71bac9cc226c73ab437b28ed310ada44090 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Tue, 1 Oct 2024 10:57:24 -0400 Subject: [PATCH 075/132] Move empty value keyword to a variable --- specifyweb/frontend/js_src/lib/components/FormCells/index.tsx | 4 ++-- specifyweb/frontend/js_src/lib/components/FormParse/index.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx index df751482253..ca6afd40365 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx @@ -18,7 +18,7 @@ import { softFail } from '../Errors/Crash'; import { fetchPathAsString } from '../Formatters/formatters'; import { UiCommand } from '../FormCommands'; import { FormField } from '../FormFields'; -import type { FormType } from '../FormParse'; +import { EMPTY_VALUE_CONDITION, FormType } from '../FormParse'; import { fetchView, resolveViewDefinition } from '../FormParse'; import type { cellAlign, @@ -237,7 +237,7 @@ const cellRenderers: { const value = await fetchPathAsString(resource, condition.field); if ( (!destructorCalled && value === condition.value) || - (condition.value === 'EMPTY' && value === '') + (condition.value === EMPTY_VALUE_CONDITION && value === '') ) { foundIndex = Number.parseInt(index); break; diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/index.ts b/specifyweb/frontend/js_src/lib/components/FormParse/index.ts index bfad30f0e1e..49f16c40914 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/index.ts @@ -468,6 +468,8 @@ export type FormCondition = | State<'Always'> | undefined; +export const EMPTY_VALUE_CONDITION="_EMPTY"; + export type ConditionalFormDefinition = RA<{ readonly condition: FormCondition; readonly definition: ParsedFormDefinition; From a757e04673529cf903a7fea3a7c3acfb3d5ece46 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Tue, 1 Oct 2024 15:01:25 +0000 Subject: [PATCH 076/132] Lint code with ESLint and Prettier Triggered by 1a81c71bac9cc226c73ab437b28ed310ada44090 on branch refs/heads/issue-5246 --- specifyweb/frontend/js_src/lib/components/FormCells/index.tsx | 3 ++- specifyweb/frontend/js_src/lib/components/FormParse/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx index ca6afd40365..6d5f96c9c97 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/index.tsx @@ -18,7 +18,8 @@ import { softFail } from '../Errors/Crash'; import { fetchPathAsString } from '../Formatters/formatters'; import { UiCommand } from '../FormCommands'; import { FormField } from '../FormFields'; -import { EMPTY_VALUE_CONDITION, FormType } from '../FormParse'; +import type { FormType } from '../FormParse'; +import { EMPTY_VALUE_CONDITION } from '../FormParse'; import { fetchView, resolveViewDefinition } from '../FormParse'; import type { cellAlign, diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/index.ts b/specifyweb/frontend/js_src/lib/components/FormParse/index.ts index 49f16c40914..a4b2f0591ac 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/index.ts @@ -468,7 +468,7 @@ export type FormCondition = | State<'Always'> | undefined; -export const EMPTY_VALUE_CONDITION="_EMPTY"; +export const EMPTY_VALUE_CONDITION = '_EMPTY'; export type ConditionalFormDefinition = RA<{ readonly condition: FormCondition; From d2e72220a6d97c3dde005a0dcf05695f3d33a54c Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:08:23 -0700 Subject: [PATCH 077/132] Remove unused localization --- specifyweb/frontend/js_src/lib/localization/forms.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 1d837f7b953..b9382efdf03 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1159,9 +1159,6 @@ export const formsText = createDictionary({ 'ru-ru': 'Номер по каталогу Числовой', 'uk-ua': 'Каталожний номер Числовий', }, - invalidTree: { - 'en-us': 'Taxon does not belong to the same tree as this Object Type', - }, addCOGChildren: { 'en-us': 'Add COG Children', }, From 217fd70181cb3c2124009be0fca46953a66f24ba Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:40:40 -0400 Subject: [PATCH 078/132] Change casing Co-authored-by: Max Patiiuk --- .../lib/components/DataModel/__tests__/businessRules.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 2e7ec80be95..5bd0b6c17bc 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -211,7 +211,7 @@ describe('Collection Object business rules', () => { }); describe('CollectionObjectGroup business rules', () => { - const getBaseCOG = () => { + const getBaseCog = () => { const cog = new tables.CollectionObjectGroup.Resource({ id: 1, cogType: getResourceApiUrl('CollectionObjectGroupType', 1), From b41828beba3ef7f09262881ef624519c31f5c14c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 2 Oct 2024 13:49:18 -0400 Subject: [PATCH 079/132] Fix casing --- .../lib/components/DataModel/__tests__/businessRules.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 5bd0b6c17bc..2f5cb63df72 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -236,7 +236,7 @@ describe('CollectionObjectGroup business rules', () => { }; test('Only one CO COJO can be primary', () => { - const { cojo1, cojo2 } = getBaseCOG(); + const { cojo1, cojo2 } = getBaseCog(); cojo1.set('isPrimary', true); expect(cojo1.get('isPrimary')).toBe(true); @@ -244,7 +244,7 @@ describe('CollectionObjectGroup business rules', () => { }); test('Only one CO COJO can be substrate', () => { - const { cojo1, cojo2 } = getBaseCOG(); + const { cojo1, cojo2 } = getBaseCog(); cojo2.set('isSubstrate', true); expect(cojo1.get('isSubstrate')).toBe(false); From c0f0887cbf77087e0013719ca44daf1211dd0bfc Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 2 Oct 2024 14:27:21 -0400 Subject: [PATCH 080/132] Change parentCojos to cojo --- .../lib/components/DataModel/__tests__/businessRules.test.ts | 2 +- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 2f5cb63df72..0700746be93 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -231,7 +231,7 @@ describe('CollectionObjectGroup business rules', () => { parentCog: getResourceApiUrl('CollectionObjectGroup', 1), }); - cog.set('parentCojos', [cojo1, cojo2]); + cog.set('cojo', [cojo1, cojo2]); return { cog, cojo1, cojo2 }; }; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 1b784702712..7aaa79daf64 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -207,7 +207,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { // The first COJO CO will automatically have isPrimary set to True when the COG type is 'consolidated' cog.rgetPromise('cogType').then((cogtype) => { if (cogtype.get('type') === cogTypes.CONSOLIDATED) { - const cojos = cog.getDependentResource('parentCojos'); + const cojos = cog.getDependentResource('cojo'); // Set first CO in COG to primary cojos?.models .find((cojo) => cojo.get('childCo') !== null) From bab5e1d853f5d457f4eec48743bb19938d8b7684 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 3 Oct 2024 15:19:50 -0500 Subject: [PATCH 081/132] Separate isComplete logic for Lazy/Relationship fetch calls --- .../DataModel/__tests__/businessRules.test.ts | 5 ++ .../lib/components/DataModel/collectionApi.ts | 60 ++++++++++--------- .../lib/components/DataModel/resourceApi.ts | 2 +- .../RecordSelectorFromCollection.tsx | 1 - 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index a3a04754ab0..48f44c23246 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -262,6 +262,11 @@ describe('uniqueness rules', () => { ]); }); + overrideAjax(getResourceApiUrl('Agent', 1), { + id: 1, + resource_uri: getResourceApiUrl('Agent', 1), + }); + test('rule with local collection', async () => { const accessionId = 1; const accession = new tables.Accession.Resource({ diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index b21230c03e1..0df0b6c1984 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -31,6 +31,33 @@ async function fakeFetch() { return this; } +async function lazyFetch(options) { + assert(this instanceof LazyCollection); + if (this._fetch) return this._fetch; + if (this.related?.isNew()) return this; + + this._neverFetched = false; + + options ||= {}; + + options.update ??= true; + options.remove ??= false; + options.silent = true; + assert(options.at == null); + + options.data = + options.data || _.extend({ domainfilter: this.domainfilter }, this.filters); + options.data.offset = options.offset ?? this.length; + options.data.orderby = options.orderby; + + _(options).has('limit') && (options.data.limit = options.limit); + this._fetch = Backbone.Collection.prototype.fetch.call(this, options); + return this._fetch.then(() => { + this._fetch = null; + return this; + }); +} + function setupToOne(collection, options) { collection.field = options.field; collection.related = options.related; @@ -129,33 +156,11 @@ export const LazyCollection = Base.extend({ return objects; }, async fetch(options) { - if (this._fetch) return this._fetch; - else if (this.isComplete() || this.related?.isNew()) return this; - - if (this.isComplete()) + if (this.isComplete()) { console.error('fetching for already filled collection'); - - this._neverFetched = false; - - options ||= {}; - - options.update ??= true; - options.remove ??= false; - options.silent = true; - assert(options.at == null); - - options.data = - options.data || - _.extend({ domainfilter: this.domainfilter }, this.filters); - options.data.offset = options.offset ?? this.length; - options.data.orderby = options.orderby; - - _(options).has('limit') && (options.data.limit = options.limit); - this._fetch = Backbone.Collection.prototype.fetch.call(this, options); - return this._fetch.then(() => { - this._fetch = null; return this; - }); + } + return lazyFetch.call(this, options); }, async fetchIfNotPopulated() { return this._neverFetched && this.related?.isNew() !== true @@ -238,9 +243,6 @@ export const IndependentCollection = LazyCollection.extend({ this.removed = new Set(); }); }, - isComplete() { - return false; - }, parse(resp) { const self = this; const records = Reflect.apply( @@ -267,7 +269,7 @@ export const IndependentCollection = LazyCollection.extend({ offset: options?.offset ?? this.getFetchOffset(), }; - return Reflect.apply(LazyCollection.prototype.fetch, this, [newOptions]); + return lazyFetch.call(this, newOptions); }, getFetchOffset() { return this.length === 0 && this.removed.size > 0 diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 1bac466e73e..6cdc5d48303 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -695,7 +695,7 @@ export const ResourceBase = Backbone.Model.extend({ console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); } else { - const fetchedToOne = toOne.isNew() ? toOne : await toOne.fetch(); + const fetchedToOne = toOne.isNew() ? toOne : toOne; this.storeIndependent(field, fetchedToOne); } } diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 0561b75bc6b..05fcc9b6acd 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -86,7 +86,6 @@ export function RecordSelectorFromCollection({ !isToOne && isLazy && collection.related?.isNew() !== true && - !collection.isComplete() && collection.models[index] === undefined ) handleFetch({ From d1cd4df5c2d9747bd1ef1ff70c0eee9ebd39e2a4 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 3 Oct 2024 20:24:40 +0000 Subject: [PATCH 082/132] Lint code with ESLint and Prettier Triggered by bab5e1d853f5d457f4eec48743bb19938d8b7684 on branch refs/heads/issue-114-backend --- .../lib/components/DataModel/collectionApi.ts | 19 +++++++------ .../lib/components/DataModel/resourceApi.ts | 28 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 0df0b6c1984..7ba93d804f4 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -4,11 +4,11 @@ import _ from 'underscore'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; +import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; -import type { AnySchema } from './helperTypes'; import { DEFAULT_FETCH_LIMIT } from './collection'; +import type { AnySchema } from './helperTypes'; import type { SpecifyResource } from './legacyTypes'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; // REFACTOR: remove @ts-nocheck @@ -213,11 +213,11 @@ export const IndependentCollection = LazyCollection.extend({ this.on( 'add', function (resource: SpecifyResource) { - if (!resource.isNew()) { - (this.removed as Set).delete(resource.url()); - this.updated[resource.cid] = resource.url(); - } else { + if (resource.isNew()) { this.updated[resource.cid] = resource; + } else { + (this.removed as ReadonlySet).delete(resource.url()); + this.updated[resource.cid] = resource.url(); } this._totalCount += 1; this.trigger('saverequired'); @@ -229,7 +229,7 @@ export const IndependentCollection = LazyCollection.extend({ 'remove', function (resource: SpecifyResource) { if (!resource.isNew()) { - (this.removed as Set).add(resource.url()); + (this.removed as ReadonlySet).add(resource.url()); } this.updated = removeKey(this.updated, resource.cid); this._totalCount -= 1; @@ -251,10 +251,11 @@ export const IndependentCollection = LazyCollection.extend({ arguments ); - this._totalCount -= (this.removed as Set).size; + this._totalCount -= (this.removed as ReadonlySet).size; return records.filter( - ({ resource_uri }) => !(this.removed as Set).has(resource_uri) + ({ resource_uri }) => + !(this.removed as ReadonlySet).has(resource_uri) ); }, async fetch(options) { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index 6cdc5d48303..f115ebfa3db 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -641,7 +641,7 @@ export const ResourceBase = Backbone.Model.extend({ return value; }); }, - async _rget( + async _rget( path: RA, options: OPTIONS ) { @@ -772,19 +772,19 @@ export const ResourceBase = Backbone.Model.extend({ console.warn('expected dependent resource to be in cache'); const collection = - existingToMany !== undefined - ? existingToMany - : this.isNew() - ? new relatedTable.DependentCollection(collectionOptions, []) - : await new relatedTable.ToOneCollection(collectionOptions) - .fetch({ limit: 0 }) - .then( - (collection) => - new relatedTable.DependentCollection( - collectionOptions, - collection.models - ) - ); + existingToMany === undefined + ? this.isNew() + ? new relatedTable.DependentCollection(collectionOptions, []) + : await new relatedTable.ToOneCollection(collectionOptions) + .fetch({ limit: 0 }) + .then( + (collection) => + new relatedTable.DependentCollection( + collectionOptions, + collection.models + ) + ) + : existingToMany; return collection.fetch({ limit: 0 }).then((collection) => { self.storeDependent(field, collection); From 1762ce330a36660688ab14bed7651304f0a06fa2 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:50:44 -0700 Subject: [PATCH 083/132] Add cojo dialog to subview --- .../FormSliders/IntegratedRecordSelector.tsx | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 36e62af0487..0d0a573a626 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -17,6 +17,8 @@ import type { import type { SpecifyResource } from '../DataModel/legacyTypes'; import { useAllSaveBlockers } from '../DataModel/saveBlockers'; import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; +import type { CollectionObjectGroup } from '../DataModel/types'; +import { COJODialog } from '../FormCells/COJODialog'; import { FormTableCollection } from '../FormCells/FormTableCollection'; import type { FormType } from '../FormParse'; import type { SubViewSortField } from '../FormParse/cells'; @@ -128,6 +130,8 @@ export function IntegratedRecordSelector({ const isAttachmentTable = collection.table.specifyTable.name.includes('Attachment'); + const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + return ( 0) - } - onClick={(): void => { - const resource = - new collection.table.specifyTable.Resource(); - - if ( - isDependent || - viewName === relationship.relatedTable.view - ) { - focusFirstField(); - handleAdd([resource]); - return; + isCOJO ? ( + + } + /> + ) : ( + 0) } + onClick={(): void => { + const resource = + new collection.table.specifyTable.Resource(); - if (state.type === 'AddResourceState') - setState({ type: 'MainState' }); - else - setState({ - type: 'AddResourceState', - resource, - handleAdd, - }); - }} - /> + if ( + isDependent || + viewName === relationship.relatedTable.view + ) { + focusFirstField(); + handleAdd([resource]); + return; + } + + if (state.type === 'AddResourceState') + setState({ type: 'MainState' }); + else + setState({ + type: 'AddResourceState', + resource, + handleAdd, + }); + }} + /> + ) ) : undefined} {hasTablePermission( relationship.relatedTable.name, From 487949b3bab20ce35438daee5a49757d5e045430 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:38:13 +0000 Subject: [PATCH 084/132] Lint code with ESLint and Prettier Triggered by 33a95ddfbb42d9c2c162296829207a198c12a6a7 on branch refs/heads/issue-5185 --- .../frontend/js_src/lib/components/DataModel/collection.ts | 2 +- .../frontend/js_src/lib/components/DataModel/specifyTable.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts index 0c5b516245d..06010a7fbe3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts @@ -10,7 +10,7 @@ import type { } from './helperTypes'; import { parseResourceUrl } from './resource'; import { serializeResource } from './serializers'; -import { Collection } from './specifyTable'; +import type { Collection } from './specifyTable'; import { genericTables, tables } from './tables'; import type { Tables } from './types'; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 4995661d1a6..a10d21a6c99 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -14,7 +14,7 @@ import { error } from '../Errors/assert'; import { attachmentView } from '../FormParse/webOnlyViews'; import { parentTableRelationship } from '../Forms/parentTables'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -import { CollectionFetchFilters } from './collection'; +import type { CollectionFetchFilters } from './collection'; import { DependentCollection, IndependentCollection, From 91b8031ec2cb2248a18bd5c9768f186fc5e93999 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Sat, 5 Oct 2024 20:50:20 -0500 Subject: [PATCH 085/132] Write frontend tests for Independent Collections --- .../DataModel/__tests__/collectionApi.test.ts | 416 +++++++++++++++++- .../lib/components/DataModel/collection.ts | 2 +- .../lib/components/DataModel/collectionApi.ts | 12 +- .../lib/components/DataModel/specifyTable.ts | 12 +- 4 files changed, 413 insertions(+), 29 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index db1747601f7..c77a618646f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -1,30 +1,33 @@ import { overrideAjax } from '../../../tests/ajax'; import { requireContext } from '../../../tests/helpers'; import { overwriteReadOnly } from '../../../utils/types'; +import type { CollectionFetchFilters } from '../collection'; +import { DEFAULT_FETCH_LIMIT } from '../collection'; +import type { AnySchema } from '../helperTypes'; import { getResourceApiUrl } from '../resource'; import type { Collection } from '../specifyTable'; import { tables } from '../tables'; -import type { Accession, Agent } from '../types'; +import type { Accession, Agent, CollectionObject } from '../types'; requireContext(); -const secondAccessionUrl = getResourceApiUrl('Accession', 12); -const accessionId = 11; -const accessionUrl = getResourceApiUrl('Accession', accessionId); -const accessionNumber = '2011-IC-116'; -const accessionsResponse = [ - { - resource_uri: accessionUrl, - id: 11, - accessionnumber: accessionNumber, - }, - { - resource_uri: secondAccessionUrl, - id: 12, - }, -]; - describe('LazyCollection', () => { + const secondAccessionUrl = getResourceApiUrl('Accession', 12); + const accessionId = 11; + const accessionUrl = getResourceApiUrl('Accession', accessionId); + const accessionNumber = '2011-IC-116'; + const accessionsResponse = [ + { + resource_uri: accessionUrl, + id: 11, + accessionnumber: accessionNumber, + }, + { + resource_uri: secondAccessionUrl, + id: 12, + }, + ]; + overrideAjax( '/api/specify/accession/?domainfilter=false&addressofrecord=4&offset=0', { @@ -71,3 +74,382 @@ describe('LazyCollection', () => { expect(collection.toJSON()).toEqual(accessionsResponse); }); }); + +describe('Independent Collection', () => { + const collectionObjectsResponse = Array.from({ length: 41 }, (_, index) => ({ + id: index + 1, + resource_uri: getResourceApiUrl('CollectionObject', index + 1), + })); + + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=1&offset=0', + { + objects: collectionObjectsResponse.slice(0, DEFAULT_FETCH_LIMIT), + meta: { + limit: DEFAULT_FETCH_LIMIT, + total_count: collectionObjectsResponse.length, + }, + } + ); + + overrideAjax( + `/api/specify/collectionobject/?domainfilter=false&accession=1&offset=${DEFAULT_FETCH_LIMIT}`, + { + objects: collectionObjectsResponse.slice( + DEFAULT_FETCH_LIMIT, + DEFAULT_FETCH_LIMIT * 2 + ), + meta: { + limit: DEFAULT_FETCH_LIMIT, + total_count: collectionObjectsResponse.length, + }, + } + ); + + overrideAjax( + `/api/specify/collectionobject/?domainfilter=false&accession=1&offset=${ + DEFAULT_FETCH_LIMIT * 2 + }`, + { + objects: collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT * 2), + meta: { + limit: DEFAULT_FETCH_LIMIT, + total_count: collectionObjectsResponse.length, + }, + } + ); + + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=1&offset=20&limit=0', + { + objects: collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT), + meta: { + limit: 0, + total_count: collectionObjectsResponse.length, + }, + } + ); + + test('lazily fetched', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + expect(collection._totalCount).toBe(collectionObjectsResponse.length); + expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse + .slice(0, DEFAULT_FETCH_LIMIT) + .map(({ id }) => id) + ); + + await collection.fetch(); + expect(collection.length).toBe(DEFAULT_FETCH_LIMIT * 2); + expect( + collection.models + .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) + .map(({ id }) => id) + ).toStrictEqual( + collectionObjectsResponse + .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) + .map(({ id }) => id) + ); + + await collection.fetch(); + expect(collection.length).toBe(collection._totalCount); + }); + + test('specified offset', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch({ + offset: DEFAULT_FETCH_LIMIT, + }); + expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse + .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) + .map(({ id }) => id) + ); + }); + + test('reset', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch({ + offset: DEFAULT_FETCH_LIMIT, + limit: 0, + }); + expect(collection.length).toBe( + collectionObjectsResponse.length - DEFAULT_FETCH_LIMIT + ); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT).map(({ id }) => id) + ); + await collection.fetch({ + reset: true, + offset: 0, + } as CollectionFetchFilters); + expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse + .slice(0, DEFAULT_FETCH_LIMIT) + .map(({ id }) => id) + ); + }); + + test('removed objects not refetched', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + const collectionObjectsToRemove = collection.models + .slice(0, 5) + .map((collectionObject) => ({ ...collectionObject })); + collectionObjectsToRemove.forEach((collectionObject) => + collection.remove(collectionObject) + ); + await collection.fetch({ offset: 0 }); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse + .slice(5, DEFAULT_FETCH_LIMIT) + .map(({ id }) => id) + ); + }); + + test('offset adjusted when all models removed', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + const collectionObjectsToRemove = collection.models.map( + (collectionObject) => ({ ...collectionObject }) + ); + collectionObjectsToRemove.forEach((collectionObject) => + collection.remove(collectionObject) + ); + expect(collection.getFetchOffset()).toBe(DEFAULT_FETCH_LIMIT); + await collection.fetch(); + expect(collection.models.map(({ id }) => id)).toStrictEqual( + collectionObjectsResponse + .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) + .map(({ id }) => id) + ); + }); + + test('on resource change event', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + + expect(collection._totalCount).toBe(collectionObjectsResponse.length); + + collection.models[0].set('text1', 'someValue'); + expect( + Object.values(collection.updated ?? {}).map((resource) => + typeof resource === 'string' ? resource : resource.toJSON() + ) + ).toStrictEqual([ + { + id: 1, + resource_uri: '/api/specify/collectionobject/1/', + text1: 'someValue', + }, + ]); + }); + + overrideAjax('/api/specify/accession/1/', { + id: 1, + resource_uri: getResourceApiUrl('Accession', 1), + }); + + overrideAjax('/api/specify/collectionobject/1/', { + id: 1, + resource_uri: getResourceApiUrl('CollectionObject', 1), + }); + + test('on change toOne', async () => { + const collectionObject = new tables.CollectionObject.Resource({ id: 1 }); + + const collection = new tables.Accession.IndependentCollection({ + related: collectionObject, + field: tables.Accession.strictGetRelationship('collectionObjects'), + }) as Collection; + + const rawAccession = new tables.Accession.Resource({ id: 1 }); + const accession = await rawAccession.fetch(); + + expect(collectionObject.get('accession')).toBeUndefined(); + collection.add(accession); + expect(collection.updated?.[accession.cid]).toBe( + getResourceApiUrl('Accession', 1) + ); + accession.set('accessionNumber', '2011-IC-116'); + expect(collection.updated?.[accession.cid]).toBe(accession); + expect(collectionObject.get('accession')).toBe( + getResourceApiUrl('Accession', 1) + ); + }); + + test('on add event', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + + const newCollectionObjects = [ + new tables.CollectionObject.Resource(), + new tables.CollectionObject.Resource({ id: 100 }), + ]; + collection.add(newCollectionObjects); + expect(collection._totalCount).toBe( + collectionObjectsResponse.length + newCollectionObjects.length + ); + expect(Object.keys(collection.updated ?? {})).toStrictEqual( + newCollectionObjects.map(({ cid }) => cid) + ); + newCollectionObjects.forEach((collectionObject) => { + const updatedEntry = collection.updated?.[collectionObject.cid]; + expect(updatedEntry).toBe( + collectionObject.isNew() ? collectionObject : collectionObject.url() + ); + }); + }); + test('on remove event', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + + const collection = await rawCollection.fetch(); + + const collectionObjectsToRemove = collection.models.slice(0, 3); + collectionObjectsToRemove.forEach((collectionObject) => + collection.remove(collectionObject) + ); + expect(collection._totalCount).toBe( + collectionObjectsResponse.length - collectionObjectsToRemove.length + ); + expect(Array.from(collection.removed ?? [])).toStrictEqual( + collectionObjectsToRemove.map((resource) => resource.get('resource_uri')) + ); + }); + test('removed and updated modify eachother', () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const collection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }) as Collection; + const collectionObject = new tables.CollectionObject.Resource({ id: 1 }); + collection.add(collectionObject); + expect(collection.updated).toStrictEqual({ + [collectionObject.cid]: collectionObject.url(), + }); + collection.remove(collectionObject); + expect(collection.removed).toStrictEqual(new Set([collectionObject.url()])); + expect(collection.updated).toStrictEqual({}); + collection.add(collectionObject); + expect(collection.updated).toStrictEqual({ + [collectionObject.cid]: collectionObject.url(), + }); + expect(collection.removed).toStrictEqual(new Set()); + }); + + overrideAjax('/api/specify/collectionobject/200/', { + id: 200, + resource_uri: getResourceApiUrl('CollectionObject', 200), + }); + + test('toApiJSON', async () => { + const accession = new tables.Accession.Resource({ + id: 1, + }); + + const rawCollection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }); + const collection = await rawCollection.fetch(); + expect(collection.toApiJSON()).toStrictEqual({ + update: [], + remove: [], + }); + const collectionObjectsToRemove = collection.models + .slice(1, 4) + .map((collectionObject) => collectionObject); + + collectionObjectsToRemove.forEach((collectionObject) => { + collection.remove(collectionObject); + }); + + const collectionObjectsToAdd = [ + new tables.CollectionObject.Resource({ id: 200 }), + new tables.CollectionObject.Resource({ text1: 'someValue' }), + ]; + collection.add(collectionObjectsToAdd); + collection.models[0].set('catalogNumber', '000000001'); + + expect(collection.toApiJSON()).toStrictEqual({ + remove: collectionObjectsToRemove.map((collectionObject) => + collectionObject.get('resource_uri') + ), + update: [ + '/api/specify/collectionobject/200/', + collection.models.at(-1), + collection.models[0], + ], + }); + }); +}); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts index 0c5b516245d..06010a7fbe3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts @@ -10,7 +10,7 @@ import type { } from './helperTypes'; import { parseResourceUrl } from './resource'; import { serializeResource } from './serializers'; -import { Collection } from './specifyTable'; +import type { Collection } from './specifyTable'; import { genericTables, tables } from './tables'; import type { Tables } from './types'; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 7ba93d804f4..df7270973ce 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -178,17 +178,15 @@ export const LazyCollection = Base.extend({ export const IndependentCollection = LazyCollection.extend({ __name__: 'IndependentCollectionBase', - constructor(options, records = []) { + constructor(options) { this.table = this.model; - assert(_.isArray(records)); - Base.call(this, records, options); + Base.call(this, null, options); this.filters = options.filters || {}; this.domainfilter = Boolean(options.domainfilter) && this.model?.specifyTable.getScopingRelationship() !== undefined; - this._totalCount = records.length; - + this._totalCount = 0; this.removed = new Set(); this.updated = {}; }, @@ -278,11 +276,9 @@ export const IndependentCollection = LazyCollection.extend({ : Math.floor(this.length / DEFAULT_FETCH_LIMIT) * DEFAULT_FETCH_LIMIT; }, toApiJSON(options) { - const self = this; - return { update: Object.values(this.updated), - remove: Array.from(self.removed), + remove: Array.from(this.removed), }; }, }); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 4995661d1a6..1869be51db3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -79,9 +79,9 @@ type CollectionConstructor = new ( ) => UnFetchedCollection; export type UnFetchedCollection = { - readonly fetch: (filter?: { - readonly limit: number; - }) => Promise>; + readonly fetch: ( + filter?: CollectionFetchFilters + ) => Promise>; }; export type Collection = { @@ -93,6 +93,8 @@ export type Collection = { readonly table: { readonly specifyTable: SpecifyTable; }; + readonly updated?: IR | string>; + readonly removed?: ReadonlySet; readonly constructor: CollectionConstructor; /* * Shorthand method signature is used to prevent @@ -103,6 +105,10 @@ export type Collection = { isComplete(): boolean; getTotalCount(): Promise; getFetchOffset(): number; + toApiJSON(): { + readonly update: RA | string>; + readonly remove: RA; + }; indexOf(resource: SpecifyResource): number; // eslint-disable-next-line @typescript-eslint/naming-convention toJSON>(): RA; From dc01fc41bef6006c6439b13e7ea6815b8e6c433e Mon Sep 17 00:00:00 2001 From: melton-jason Date: Sun, 6 Oct 2024 01:54:15 +0000 Subject: [PATCH 086/132] Lint code with ESLint and Prettier Triggered by 91b8031ec2cb2248a18bd5c9768f186fc5e93999 on branch refs/heads/issue-114-backend --- .../DataModel/__tests__/collectionApi.test.ts | 12 ++++++------ .../js_src/lib/components/DataModel/specifyTable.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index c77a618646f..42735c60009 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -142,7 +142,7 @@ describe('Independent Collection', () => { const collection = await rawCollection.fetch(); expect(collection._totalCount).toBe(collectionObjectsResponse.length); - expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); expect(collection.models.map(({ id }) => id)).toStrictEqual( collectionObjectsResponse .slice(0, DEFAULT_FETCH_LIMIT) @@ -150,7 +150,7 @@ describe('Independent Collection', () => { ); await collection.fetch(); - expect(collection.length).toBe(DEFAULT_FETCH_LIMIT * 2); + expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT * 2); expect( collection.models .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) @@ -162,7 +162,7 @@ describe('Independent Collection', () => { ); await collection.fetch(); - expect(collection.length).toBe(collection._totalCount); + expect(collection).toHaveLength(collection._totalCount); }); test('specified offset', async () => { @@ -178,7 +178,7 @@ describe('Independent Collection', () => { const collection = await rawCollection.fetch({ offset: DEFAULT_FETCH_LIMIT, }); - expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); expect(collection.models.map(({ id }) => id)).toStrictEqual( collectionObjectsResponse .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) @@ -200,7 +200,7 @@ describe('Independent Collection', () => { offset: DEFAULT_FETCH_LIMIT, limit: 0, }); - expect(collection.length).toBe( + expect(collection).toHaveLength( collectionObjectsResponse.length - DEFAULT_FETCH_LIMIT ); expect(collection.models.map(({ id }) => id)).toStrictEqual( @@ -210,7 +210,7 @@ describe('Independent Collection', () => { reset: true, offset: 0, } as CollectionFetchFilters); - expect(collection.length).toBe(DEFAULT_FETCH_LIMIT); + expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); expect(collection.models.map(({ id }) => id)).toStrictEqual( collectionObjectsResponse .slice(0, DEFAULT_FETCH_LIMIT) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index 1869be51db3..e7abde2361e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -14,7 +14,7 @@ import { error } from '../Errors/assert'; import { attachmentView } from '../FormParse/webOnlyViews'; import { parentTableRelationship } from '../Forms/parentTables'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -import { CollectionFetchFilters } from './collection'; +import type { CollectionFetchFilters } from './collection'; import { DependentCollection, IndependentCollection, From 3a6805c8329aed4963c7063300a21f31551ffb2d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 8 Oct 2024 09:30:00 -0700 Subject: [PATCH 087/132] Check totalCount --- .../lib/components/DataModel/__tests__/collectionApi.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index 42735c60009..75a7bbfa96a 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -162,7 +162,8 @@ describe('Independent Collection', () => { ); await collection.fetch(); - expect(collection).toHaveLength(collection._totalCount); + const totalCount = collection._totalCount ?? 0; + expect(collection).toHaveLength(totalCount); }); test('specified offset', async () => { From 5a9fe94928adf697180547f52235ea0eb4040b37 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 8 Oct 2024 14:42:17 -0500 Subject: [PATCH 088/132] Always respect options to collection fetch calls --- .../lib/components/DataModel/collectionApi.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index df7270973ce..7effed034b9 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -27,14 +27,19 @@ function notSupported() { throw new Error('method is not supported'); } -async function fakeFetch() { +async function fakeFetch(rawOptions) { + const options = { + ...rawOptions, + }; + if (typeof options.success === 'function') + options.success.call(options.context, this, undefined, options); return this; } async function lazyFetch(options) { assert(this instanceof LazyCollection); if (this._fetch) return this._fetch; - if (this.related?.isNew()) return this; + if (this.related?.isNew()) return fakeFetch.call(this, options); this._neverFetched = false; @@ -119,7 +124,9 @@ export const DependentCollection = Base.extend({ getFetchOffset() { return 0; }, - fetch: fakeFetch, + async fetch(options) { + return fakeFetch.call(this, options); + }, sync: notSupported, create: notSupported, }); @@ -258,7 +265,7 @@ export const IndependentCollection = LazyCollection.extend({ }, async fetch(options) { // If the related is being fetched, don't try and fetch the collection - if (this.related._fetch !== null) return this; + if (this.related._fetch !== null) return fakeFetch.call(this, options); this.filters[this.field.name.toLowerCase()] = this.related.id; From d3821701a4c039df7edb57376603654bab9bb5af Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 8 Oct 2024 14:59:32 -0500 Subject: [PATCH 089/132] Write test for collection fetch options --- .../DataModel/__tests__/collectionApi.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index 42735c60009..1c88f40f0a7 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -407,6 +407,24 @@ describe('Independent Collection', () => { expect(collection.removed).toStrictEqual(new Set()); }); + test('success options respected', async () => { + const accession = new tables.Accession.Resource(); + + expect(accession.isNew()).toBe(true); + + const collection = new tables.CollectionObject.IndependentCollection({ + related: accession, + field: tables.CollectionObject.strictGetRelationship('accession'), + }) as Collection; + + await collection.fetch({ + success: (collection) => { + collection.add(new tables.CollectionObject.Resource()); + }, + } as CollectionFetchFilters); + expect(collection.models).toHaveLength(1); + }); + overrideAjax('/api/specify/collectionobject/200/', { id: 200, resource_uri: getResourceApiUrl('CollectionObject', 200), From ad2a9d53fa1283b4e38fa308313a6ed1704fd757 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 8 Oct 2024 15:05:40 -0500 Subject: [PATCH 090/132] Fix type error in test --- .../lib/components/DataModel/__tests__/collectionApi.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index 1c88f40f0a7..3ea9b454cea 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -162,7 +162,8 @@ describe('Independent Collection', () => { ); await collection.fetch(); - expect(collection).toHaveLength(collection._totalCount); + // eslint-disable-next-line jest/prefer-to-have-length + expect(collection.length).toBe(collection._totalCount); }); test('specified offset', async () => { From a5f0425b7cc6560dd2fbbf7e11c7c9b7ea4c3759 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:38:55 -0700 Subject: [PATCH 091/132] Change child field --- .../js_src/lib/components/FormCells/COJODialog.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 8f7c1d6bd9b..4ba91d82cc1 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -101,10 +101,10 @@ export function COJODialog({ const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); const field = newResource.specifyTable.name === 'CollectionObject' - ? 'childco' - : 'childcog'; + ? 'childCo' + : 'childCog'; newCOJO.set(field, newResource as never); - newCOJO.set('parentcog', parentResource); + newCOJO.set('parentCog', parentResource as never); collection?.add(newCOJO); setState(undefined); setResource(undefined); @@ -127,10 +127,10 @@ export function COJODialog({ const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); const field = selectedResource.specifyTable.name === 'CollectionObject' - ? 'childco' - : 'childcog'; + ? 'childCo' + : 'childCog'; newCOJO.set(field, selectedResource as never); - newCOJO.set('parentcog', parentResource); + newCOJO.set('parentCog', parentResource as never); collection?.add(newCOJO); setState(undefined); handleClose(); From 5fe79564c2f9704623965a1bc96e0ce02b2f4e70 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:42:04 -0700 Subject: [PATCH 092/132] Research and tries --- .../lib/components/DataModel/resourceApi.ts | 1 + .../lib/components/FormCells/COJODialog.tsx | 23 ++++++++- .../lib/components/FormCells/FormTable.tsx | 51 ++++++++++--------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index f115ebfa3db..e9942335f86 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -64,6 +64,7 @@ function eventHandlerForToOne(related, field) { function eventHandlerForToMany(_related, field) { return function (event) { + console.log('eventHandlerForToMany', field); const args = _.toArray(arguments); switch (event) { case 'changing': { diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 4ba91d82cc1..7f5bac3e601 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; +import type { RA } from '../../utils/types'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -103,9 +104,27 @@ export function COJODialog({ newResource.specifyTable.name === 'CollectionObject' ? 'childCo' : 'childCog'; - newCOJO.set(field, newResource as never); - newCOJO.set('parentCog', parentResource as never); + // NewResource.set('cojo', newCOJO); + void newResource.save(); + const newResourceUrl = newResource.url(); + newCOJO.set(field, newResourceUrl as never); + /* + * Might not need to set the parent cog here, can do it with business rules on the main COG form when saving that + * NewCOJO.set('parentCog', parentResource as never); ==> this creates the infinite loop + */ + /* + * Const field = + * newResource.specifyTable.name === 'CollectionObject' + * ? 'childCo' + * : 'childCog'; + * newCOJO.set(field, newResource as never); + * newCOJO.set('parentCog', parentResource as never); + */ collection?.add(newCOJO); + /* + * HandleAdd([newCOJO]); + * Collection?.add(newCOJO); + */ setState(undefined); setResource(undefined); handleClose(); diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index c9610c547c5..0102817ca2a 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -456,31 +456,32 @@ export function FormTable({ const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; - const addButtons = isCOJO ? ( - - } - /> - ) : typeof handleAddResources === 'function' && - mode !== 'view' && - !disableAdding ? ( - <> - {!isDependent && - hasTablePermission(relationship.relatedTable.name, 'read') ? ( - - ) : undefined} - {hasTablePermission(relationship.relatedTable.name, 'create') ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - }} - /> - ) : undefined} - - ) : undefined; + const addButtons = + isCOJO && typeof handleAddResources === 'function' ? ( + + } + /> + ) : typeof handleAddResources === 'function' && + mode !== 'view' && + !disableAdding ? ( + <> + {!isDependent && + hasTablePermission(relationship.relatedTable.name, 'read') ? ( + + ) : undefined} + {hasTablePermission(relationship.relatedTable.name, 'create') ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + }} + /> + ) : undefined} + + ) : undefined; return dialog === false ? ( From c60941f8cae84c9ea29e2dc96226eb725444c17e Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:25:16 -0700 Subject: [PATCH 093/132] Add parentUrl --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 7f5bac3e601..6dd7a98ce1c 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -107,7 +107,9 @@ export function COJODialog({ // NewResource.set('cojo', newCOJO); void newResource.save(); const newResourceUrl = newResource.url(); + const parentResourceUrl = parentResource.url(); newCOJO.set(field, newResourceUrl as never); + newCOJO.set('parentCog', parentResourceUrl as never); /* * Might not need to set the parent cog here, can do it with business rules on the main COG form when saving that * NewCOJO.set('parentCog', parentResource as never); ==> this creates the infinite loop From 6c93b08f919bd3d458afb6178783648de66c335d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:50:47 -0700 Subject: [PATCH 094/132] Handle mutliple COs added at same time --- .../lib/components/FormCells/COJODialog.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 6dd7a98ce1c..08556b1bf25 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -135,25 +135,34 @@ export function COJODialog({ /> ) : undefined} {state === 'Search' && - resource !== undefined && + newResource !== undefined && parentResource !== undefined ? ( } onClose={(): void => setState(undefined)} - onSelected={([selectedResource]): void => { - const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); - const field = - selectedResource.specifyTable.name === 'CollectionObject' - ? 'childCo' - : 'childCog'; - newCOJO.set(field, selectedResource as never); - newCOJO.set('parentCog', parentResource as never); - collection?.add(newCOJO); + onSelected={(selectedResources): void => { + selectedResources.forEach((selectedResource) => { + const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); + const field = + selectedResource.specifyTable.name === 'CollectionObject' + ? 'childCo' + : 'childCog'; + + const selectedResourceUrl = selectedResource.url(); + const parentResourceUrl = parentResource.url(); + + newCOJO.set(field, selectedResourceUrl as never); + newCOJO.set('parentCog', parentResourceUrl as never); + + collection?.add(newCOJO); + }); + setState(undefined); + setResource(undefined); handleClose(); }} /> From 3a274cf518eeafa8b0426070745e2bd65beafc5a Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:53:45 -0700 Subject: [PATCH 095/132] Remove some comments --- .../lib/components/FormCells/COJODialog.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 08556b1bf25..7370ff5d9ab 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -104,29 +104,13 @@ export function COJODialog({ newResource.specifyTable.name === 'CollectionObject' ? 'childCo' : 'childCog'; - // NewResource.set('cojo', newCOJO); + // NewResource.set('cojo', newCOJO); Do this in bus rule when saving main COG? void newResource.save(); const newResourceUrl = newResource.url(); const parentResourceUrl = parentResource.url(); newCOJO.set(field, newResourceUrl as never); newCOJO.set('parentCog', parentResourceUrl as never); - /* - * Might not need to set the parent cog here, can do it with business rules on the main COG form when saving that - * NewCOJO.set('parentCog', parentResource as never); ==> this creates the infinite loop - */ - /* - * Const field = - * newResource.specifyTable.name === 'CollectionObject' - * ? 'childCo' - * : 'childCog'; - * newCOJO.set(field, newResource as never); - * newCOJO.set('parentCog', parentResource as never); - */ collection?.add(newCOJO); - /* - * HandleAdd([newCOJO]); - * Collection?.add(newCOJO); - */ setState(undefined); setResource(undefined); handleClose(); From f9d1303bdc09f70a607b0cde86394310cd306b41 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:21:50 -0700 Subject: [PATCH 096/132] Remove duplication --- .../lib/components/FormCells/COJODialog.tsx | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 7370ff5d9ab..b7177551d1d 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -51,6 +51,42 @@ export function COJODialog({ setNewResource(createdResource); } }, [resource]); + + const handleCOJOCreation = ( + selectedResource?: SpecifyResource + ): void => { + // NewResource.set('cojo', newCOJO); Do this in bus rule when saving main COG? + if (parentResource === undefined) return; + + const resourceToUse = selectedResource ?? newResource; + + if (resourceToUse === undefined) return; + + if (newResource) { + void newResource.save(); + } + + const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); + const field = + resourceToUse.specifyTable.name === 'CollectionObject' + ? 'childCo' + : 'childCog'; + + const resourceUrl = resourceToUse.url(); + const parentResourceUrl = parentResource.url(); + + newCOJO.set(field, resourceUrl as never); + newCOJO.set('parentCog', parentResourceUrl as never); + + collection?.add(newCOJO); + }; + + const handleStates = (): void => { + setState(undefined); + setResource(undefined); + handleClose(); + }; + return ( <> @@ -99,28 +135,13 @@ export function COJODialog({ }} onDeleted={undefined} onSaved={(): void => { - const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); - const field = - newResource.specifyTable.name === 'CollectionObject' - ? 'childCo' - : 'childCog'; - // NewResource.set('cojo', newCOJO); Do this in bus rule when saving main COG? - void newResource.save(); - const newResourceUrl = newResource.url(); - const parentResourceUrl = parentResource.url(); - newCOJO.set(field, newResourceUrl as never); - newCOJO.set('parentCog', parentResourceUrl as never); - collection?.add(newCOJO); - setState(undefined); - setResource(undefined); - handleClose(); + handleCOJOCreation(); + handleStates(); }} onSaving={undefined} /> ) : undefined} - {state === 'Search' && - newResource !== undefined && - parentResource !== undefined ? ( + {state === 'Search' && parentResource !== undefined ? ( setState(undefined)} onSelected={(selectedResources): void => { selectedResources.forEach((selectedResource) => { - const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); - const field = - selectedResource.specifyTable.name === 'CollectionObject' - ? 'childCo' - : 'childCog'; - - const selectedResourceUrl = selectedResource.url(); - const parentResourceUrl = parentResource.url(); - - newCOJO.set(field, selectedResourceUrl as never); - newCOJO.set('parentCog', parentResourceUrl as never); - - collection?.add(newCOJO); + handleCOJOCreation(selectedResource); }); - - setState(undefined); - setResource(undefined); - handleClose(); + handleStates(); }} /> ) : undefined} From 6300e573772caaad5f6817a38062f4249c4d8ab9 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:23:09 -0700 Subject: [PATCH 097/132] Remove log --- .../frontend/js_src/lib/components/DataModel/resourceApi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index e9942335f86..f115ebfa3db 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -64,7 +64,6 @@ function eventHandlerForToOne(related, field) { function eventHandlerForToMany(_related, field) { return function (event) { - console.log('eventHandlerForToMany', field); const args = _.toArray(arguments); switch (event) { case 'changing': { From 656518e05698ce024fef6e0d7c7050cd72b5262d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:25:29 -0700 Subject: [PATCH 098/132] Remove import --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index b7177551d1d..bafb579c068 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; -import type { RA } from '../../utils/types'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -47,7 +46,9 @@ export function COJODialog({ React.useEffect(() => { if (resource !== undefined) { - const createdResource = new resource.Resource(); + const createdResource = new resource.Resource() as + | SpecifyResource + | SpecifyResource; setNewResource(createdResource); } }, [resource]); From f41106955d6bbd321b147074daa31a85c764186f Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:39:48 -0700 Subject: [PATCH 099/132] Set cojo on parent --- .../lib/components/DataModel/businessRuleDefs.ts | 1 - .../js_src/lib/components/FormCells/COJODialog.tsx | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 328c12df255..48b74ae9bdc 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -193,7 +193,6 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }, }, }, - Determination: { fieldChecks: { taxon: async ( diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index bafb579c068..aa85785b9ae 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; +import type { RA } from '../../utils/types'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -12,6 +13,7 @@ import { tables } from '../DataModel/tables'; import type { CollectionObject, CollectionObjectGroup, + CollectionObjectGroupJoin, } from '../DataModel/types'; import { ResourceView } from '../Forms/ResourceView'; import { Dialog } from '../Molecules/Dialog'; @@ -80,6 +82,14 @@ export function COJODialog({ newCOJO.set('parentCog', parentResourceUrl as never); collection?.add(newCOJO); + + /* + * ParentResource.set('cojo', [newCOJO] as RA< + * SpecifyResource + * >); + */ + const parentResourceCojo = parentResource.getDependentResource('cojo'); + if (typeof parentResourceCojo === 'object') parentResourceCojo.add(newCOJO); }; const handleStates = (): void => { From 5add530647b19660f0cae07942468591b9fad08c Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:00:18 -0700 Subject: [PATCH 100/132] Remove code --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index aa85785b9ae..01a9ca8366b 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -83,11 +83,6 @@ export function COJODialog({ collection?.add(newCOJO); - /* - * ParentResource.set('cojo', [newCOJO] as RA< - * SpecifyResource - * >); - */ const parentResourceCojo = parentResource.getDependentResource('cojo'); if (typeof parentResourceCojo === 'object') parentResourceCojo.add(newCOJO); }; From 6c0174663ae70403dd9b4ac95f00f0a13348bba3 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Thu, 17 Oct 2024 07:51:59 -0700 Subject: [PATCH 101/132] Remove import --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 01a9ca8366b..2562871d206 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; -import type { RA } from '../../utils/types'; import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -13,7 +12,6 @@ import { tables } from '../DataModel/tables'; import type { CollectionObject, CollectionObjectGroup, - CollectionObjectGroupJoin, } from '../DataModel/types'; import { ResourceView } from '../Forms/ResourceView'; import { Dialog } from '../Molecules/Dialog'; From 929060262333cd72dba07591a20d74a79e032404 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 17 Oct 2024 11:49:40 -0500 Subject: [PATCH 102/132] Datamodel improvements --- .../js_src/lib/components/DataModel/types.ts | 210 +++++++++--------- specifyweb/specify/api.py | 2 +- specifyweb/specify/datamodel.py | 5 +- specifyweb/specify/models.py | 2 +- 4 files changed, 114 insertions(+), 105 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index abbf2f321ec..93c0e575828 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -229,24 +229,33 @@ export type Tables = { readonly CollectionObjectGroup: CollectionObjectGroup; readonly CollectionObjectGroupJoin: CollectionObjectGroupJoin; readonly CollectionObjectGroupType: CollectionObjectGroupType; + readonly AbsoluteAge: AbsoluteAge; + readonly RelativeAge: RelativeAge; + readonly AbsoluteAgeAttachment: AbsoluteAgeAttachment; + readonly RelativeAgeAttachment: RelativeAgeAttachment; + readonly AbsoluteAgeCitation: AbsoluteAgeCitation; + readonly RelativeAgeCitation: RelativeAgeCitation; + readonly TectonicUnitTreeDef: TectonicUnitTreeDef; + readonly TectonicUnitTreeDefItem: TectonicUnitTreeDefItem; + readonly TectonicUnit: TectonicUnit; }; export type Accession = { readonly tableName: 'Accession'; readonly fields: { - readonly accessionCondition: string | null; readonly accessionNumber: string; + readonly accessionCondition: string | null; + readonly dateAccessioned: string | null; readonly actualTotalCountAmt: number | null; readonly collectionObjectCount: number | null; - readonly dateAccessioned: string | null; readonly dateAcknowledged: string | null; - readonly dateReceived: string | null; + readonly remarks: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; readonly number1: number | null; readonly number2: number | null; readonly preparationCount: number | null; - readonly remarks: string | null; + readonly dateReceived: string | null; readonly status: string | null; readonly text1: string | null; readonly text2: string | null; @@ -452,10 +461,10 @@ export type Agent = { readonly initials: string | null; readonly integer1: number | null; readonly integer2: number | null; - readonly interests: string | null; readonly jobTitle: string | null; readonly lastName: string | null; readonly middleInitial: string | null; + readonly interests: string | null; readonly remarks: string | null; readonly suffix: string | null; readonly text1: string | null; @@ -488,8 +497,8 @@ export type Agent = { readonly agentAttachments: RA; readonly agentGeographies: RA; readonly agentSpecialties: RA; - readonly groups: RA; readonly identifiers: RA; + readonly groups: RA; readonly variants: RA; }; readonly toManyIndependent: { @@ -920,13 +929,13 @@ export type BorrowMaterial = { readonly tableName: 'BorrowMaterial'; readonly fields: { readonly collectionMemberId: number; - readonly description: string | null; readonly inComments: string | null; readonly materialNumber: string; readonly outComments: string | null; readonly quantity: number | null; readonly quantityResolved: number | null; readonly quantityReturned: number | null; + readonly description: string | null; readonly text1: string | null; readonly text2: string | null; readonly timestampCreated: string; @@ -973,12 +982,12 @@ export type CollectingEvent = { readonly endDatePrecision: number | null; readonly endDateVerbatim: string | null; readonly endTime: number | null; + readonly stationFieldNumber: string | null; + readonly method: string | null; readonly guid: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly remarks: string | null; - readonly method: string | null; - readonly stationFieldNumber: string | null; readonly reservedInteger3: number | null; readonly reservedInteger4: number | null; readonly reservedText1: string | null; @@ -1070,6 +1079,10 @@ export type CollectingEventAttr = { export type CollectingEventAttribute = { readonly tableName: 'CollectingEventAttribute'; readonly fields: { + readonly text8: string | null; + readonly text5: string | null; + readonly text4: string | null; + readonly text9: string | null; readonly integer1: number | null; readonly integer10: number | null; readonly integer2: number | null; @@ -1080,11 +1093,11 @@ export type CollectingEventAttribute = { readonly integer7: number | null; readonly integer8: number | null; readonly integer9: number | null; + readonly number12: number | null; + readonly number13: number | null; readonly number1: number | null; readonly number10: number | null; readonly number11: number | null; - readonly number12: number | null; - readonly number13: number | null; readonly number2: number | null; readonly number3: number | null; readonly number4: number | null; @@ -1094,26 +1107,22 @@ export type CollectingEventAttribute = { readonly number8: number | null; readonly number9: number | null; readonly remarks: string | null; + readonly text6: string | null; readonly text1: string | null; readonly text10: string | null; readonly text11: string | null; - readonly text12: string | null; readonly text13: string | null; readonly text14: string | null; readonly text15: string | null; readonly text16: string | null; readonly text17: string | null; readonly text2: string | null; - readonly text3: string | null; - readonly text4: string | null; - readonly text5: string | null; - readonly text6: string | null; readonly text7: string | null; - readonly text8: string | null; - readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; + readonly text12: string | null; readonly version: number | null; + readonly text3: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; @@ -1154,6 +1163,7 @@ export type CollectingTrip = { readonly tableName: 'CollectingTrip'; readonly fields: { readonly cruise: string | null; + readonly text2: string | null; readonly date1: string | null; readonly date1Precision: number | null; readonly date2: string | null; @@ -1171,8 +1181,6 @@ export type CollectingTrip = { readonly startDatePrecision: number | null; readonly startDateVerbatim: string | null; readonly startTime: number | null; - readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -1184,6 +1192,7 @@ export type CollectingTrip = { readonly timestampModified: string | null; readonly collectingTripName: string | null; readonly version: number | null; + readonly text1: string | null; readonly vessel: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; @@ -1359,51 +1368,51 @@ export type Collection = { export type CollectionObject = { readonly tableName: 'CollectionObject'; readonly fields: { - readonly modifier: string | null; - readonly catalogedDate: string | null; - readonly projectNumber: string | null; readonly actualTotalCountAmt: number | null; readonly age: number | null; readonly availability: string | null; + readonly catalogNumber: string | null; + readonly catalogedDate: string | null; readonly catalogedDatePrecision: number | null; readonly catalogedDateVerbatim: string | null; readonly collectionMemberId: number; readonly countAmt: number | null; + readonly reservedText: string | null; + readonly timestampModified: string | null; readonly date1: string | null; readonly date1Precision: number | null; readonly deaccessioned: boolean | null; - readonly timestampModified: string | null; readonly embargoReason: string | null; readonly embargoReleaseDate: string | null; readonly embargoReleaseDatePrecision: number | null; readonly embargoStartDate: string | null; readonly embargoStartDatePrecision: number | null; - readonly yesNo2: boolean | null; - readonly fieldNumber: string | null; readonly guid: string | null; - readonly text2: string | null; readonly integer1: number | null; readonly integer2: number | null; + readonly text2: string | null; readonly inventoryDate: string | null; readonly inventoryDatePrecision: number | null; - readonly description: string | null; + readonly isMemberOfCOG: boolean | null; + readonly modifier: string | null; + readonly name: string | null; readonly notifications: string | null; readonly numberOfDuplicates: number | null; + readonly number1: number | null; + readonly number2: number | null; readonly objectCondition: string | null; readonly ocr: string | null; - readonly name: string | null; readonly altCatalogNumber: string | null; - readonly text1: string | null; - readonly yesNo1: boolean | null; + readonly projectNumber: string | null; readonly remarks: string | null; readonly reservedInteger3: number | null; readonly reservedInteger4: number | null; - readonly reservedText: string | null; readonly reservedText2: string | null; readonly reservedText3: string | null; readonly restrictions: string | null; readonly sgrStatus: number | null; - readonly catalogNumber: string | null; + readonly text1: string | null; + readonly description: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -1414,10 +1423,11 @@ export type CollectionObject = { readonly totalCountAmt: number | null; readonly totalValue: number | null; readonly uniqueIdentifier: string | null; - readonly number1: number | null; readonly version: number | null; readonly visibility: number | null; - readonly number2: number | null; + readonly fieldNumber: string | null; + readonly yesNo1: boolean | null; + readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; readonly yesNo4: boolean | null; readonly yesNo5: boolean | null; @@ -1430,16 +1440,16 @@ export type CollectionObject = { readonly accession: Accession | null; readonly agent1: Agent | null; readonly appraisal: Appraisal | null; - readonly collectingEvent: CollectingEvent | null; + readonly cataloger: Agent | null; readonly collection: Collection; readonly collectionObjectType: CollectionObjectType; readonly container: Container | null; readonly containerOwner: Container | null; readonly createdByAgent: Agent | null; readonly currentDetermination: Determination | null; - readonly cataloger: Agent | null; readonly modifiedByAgent: Agent | null; readonly embargoAuthority: Agent | null; + readonly collectingEvent: CollectingEvent | null; readonly fieldNotebookPage: FieldNotebookPage | null; readonly inventorizedBy: Agent | null; readonly paleoContext: PaleoContext | null; @@ -1508,23 +1518,12 @@ export type CollectionObjectAttr = { export type CollectionObjectAttribute = { readonly tableName: 'CollectionObjectAttribute'; readonly fields: { - readonly number1: number | null; - readonly text4: string | null; readonly bottomDistance: number | null; readonly collectionMemberId: number; - readonly text11: string | null; - readonly text12: string | null; - readonly text10: string | null; - readonly text2: string | null; - readonly number37: number | null; readonly date1: string | null; readonly date1Precision: number | null; - readonly remarks: string | null; - readonly text15: string | null; readonly direction: string | null; readonly distanceUnits: string | null; - readonly text6: string | null; - readonly number2: number | null; readonly integer1: number | null; readonly integer10: number | null; readonly integer2: number | null; @@ -1535,18 +1534,18 @@ export type CollectionObjectAttribute = { readonly integer7: number | null; readonly integer8: number | null; readonly integer9: number | null; - readonly number38: number | null; - readonly text7: string | null; - readonly number10: number | null; - readonly number11: number | null; readonly number12: number | null; readonly number13: number | null; + readonly number1: number | null; + readonly number10: number | null; + readonly number11: number | null; readonly number14: number | null; readonly number15: number | null; readonly number16: number | null; readonly number17: number | null; readonly number18: number | null; readonly number19: number | null; + readonly number2: number | null; readonly number20: number | null; readonly number21: number | null; readonly number22: number | null; @@ -1565,20 +1564,27 @@ export type CollectionObjectAttribute = { readonly number34: number | null; readonly number35: number | null; readonly number36: number | null; + readonly number37: number | null; + readonly number38: number | null; readonly number39: number | null; readonly number4: number | null; readonly number40: number | null; + readonly number41: number | null; readonly number42: number | null; readonly number5: number | null; readonly number6: number | null; readonly number7: number | null; readonly number8: number | null; readonly number9: number | null; - readonly positionState: string | null; - readonly text3: string | null; - readonly text5: string | null; readonly text13: string | null; readonly text14: string | null; + readonly text1: string | null; + readonly positionState: string | null; + readonly text10: string | null; + readonly remarks: string | null; + readonly text8: string | null; + readonly text11: string | null; + readonly text15: string | null; readonly text16: string | null; readonly text17: string | null; readonly text18: string | null; @@ -1593,6 +1599,7 @@ export type CollectionObjectAttribute = { readonly text27: string | null; readonly text28: string | null; readonly text29: string | null; + readonly text3: string | null; readonly text30: string | null; readonly text31: string | null; readonly text32: string | null; @@ -1603,15 +1610,18 @@ export type CollectionObjectAttribute = { readonly text37: string | null; readonly text38: string | null; readonly text39: string | null; + readonly text4: string | null; readonly text40: string | null; + readonly text5: string | null; + readonly text6: string | null; + readonly text7: string | null; readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; + readonly text12: string | null; readonly topDistance: number | null; - readonly text1: string | null; readonly version: number | null; - readonly text8: string | null; - readonly number41: number | null; + readonly text2: string | null; readonly yesNo1: boolean | null; readonly yesNo10: boolean | null; readonly yesNo11: boolean | null; @@ -2185,7 +2195,9 @@ export type DNASequence = { readonly compT: number | null; readonly extractionDate: string | null; readonly extractionDatePrecision: number | null; + readonly text2: string | null; readonly genbankAccessionNumber: string | null; + readonly text1: string | null; readonly geneSequence: string | null; readonly moleculeType: string | null; readonly number1: number | null; @@ -2195,8 +2207,6 @@ export type DNASequence = { readonly sequenceDate: string | null; readonly sequenceDatePrecision: number | null; readonly targetMarker: string | null; - readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; @@ -2289,8 +2299,8 @@ export type DNASequencingRun = { readonly runByAgent: Agent | null; }; readonly toManyDependent: { - readonly attachments: RA; readonly citations: RA; + readonly attachments: RA; }; readonly toManyIndependent: RR; }; @@ -2359,10 +2369,11 @@ export type DataType = { export type Deaccession = { readonly tableName: 'Deaccession'; readonly fields: { + readonly timestampModified: string | null; readonly date1: string | null; readonly date2: string | null; - readonly deaccessionDate: string | null; readonly deaccessionNumber: string; + readonly deaccessionDate: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -2381,7 +2392,6 @@ export type Deaccession = { readonly text4: string | null; readonly text5: string | null; readonly timestampCreated: string; - readonly timestampModified: string | null; readonly totalItems: number | null; readonly totalPreps: number | null; readonly type: string | null; @@ -2450,14 +2460,15 @@ export type Determination = { readonly tableName: 'Determination'; readonly fields: { readonly addendum: string | null; - readonly text1: string | null; readonly alternateName: string | null; readonly collectionMemberId: number; readonly confidence: string | null; readonly isCurrent: boolean; readonly determinedDate: string | null; readonly determinedDatePrecision: number | null; + readonly featureOrBasis: string | null; readonly guid: string | null; + readonly yesNo1: boolean | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -2473,6 +2484,8 @@ export type Determination = { readonly qualifier: string | null; readonly remarks: string | null; readonly subSpQualifier: string | null; + readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -2481,12 +2494,9 @@ export type Determination = { readonly text8: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly text2: string | null; readonly typeStatusName: string | null; readonly varQualifier: string | null; - readonly featureOrBasis: string | null; readonly version: number | null; - readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; readonly yesNo4: boolean | null; @@ -2498,8 +2508,8 @@ export type Determination = { readonly createdByAgent: Agent | null; readonly determiner: Agent | null; readonly modifiedByAgent: Agent | null; - readonly taxon: Taxon | null; readonly preferredTaxon: Taxon | null; + readonly taxon: Taxon | null; }; readonly toManyDependent: { readonly determinationCitations: RA; @@ -3337,8 +3347,8 @@ export type Gift = { readonly srcTaxonomy: string | null; readonly specialConditions: string | null; readonly status: string | null; - readonly text1: string | null; readonly text2: string | null; + readonly text1: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3707,6 +3717,7 @@ export type Loan = { readonly dateClosed: string | null; readonly dateReceived: string | null; readonly yesNo1: boolean | null; + readonly text2: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -3728,7 +3739,6 @@ export type Loan = { readonly specialConditions: string | null; readonly status: string | null; readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3854,11 +3864,8 @@ export type LoanReturnPreparation = { export type Locality = { readonly tableName: 'Locality'; readonly fields: { - readonly text1: string | null; - readonly text2: string | null; readonly datum: string | null; readonly elevationAccuracy: number | null; - readonly elevationMethod: string | null; readonly gml: string | null; readonly guid: string | null; readonly latLongMethod: string | null; @@ -3883,6 +3890,8 @@ export type Locality = { readonly sgrStatus: number | null; readonly shortName: string | null; readonly srcLatLongUnit: number; + readonly text1: string | null; + readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3894,6 +3903,7 @@ export type Locality = { readonly verbatimLongitude: string | null; readonly version: number | null; readonly visibility: number | null; + readonly elevationMethod: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; @@ -4387,12 +4397,10 @@ export type Preparation = { readonly sampleNumber: string | null; readonly status: string | null; readonly storageLocation: string | null; - readonly text1: string | null; readonly text10: string | null; readonly text11: string | null; readonly text12: string | null; readonly text13: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -4402,8 +4410,10 @@ export type Preparation = { readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly version: number | null; + readonly text1: string | null; readonly yesNo1: boolean | null; + readonly version: number | null; + readonly text2: string | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; }; @@ -4780,7 +4790,10 @@ export type RecordSetItem = { export type ReferenceWork = { readonly tableName: 'ReferenceWork'; readonly fields: { + readonly text1: string | null; + readonly workDate: string | null; readonly doi: string | null; + readonly text2: string | null; readonly guid: string | null; readonly isPublished: boolean | null; readonly isbn: string | null; @@ -4791,8 +4804,6 @@ export type ReferenceWork = { readonly placeOfPublication: string | null; readonly publisher: string | null; readonly remarks: string | null; - readonly text1: string | null; - readonly text2: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly title: string; @@ -4801,7 +4812,6 @@ export type ReferenceWork = { readonly url: string | null; readonly version: number | null; readonly volume: string | null; - readonly workDate: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; }; @@ -4899,9 +4909,9 @@ export type RepositoryAgreementAttachment = { export type Shipment = { readonly tableName: 'Shipment'; readonly fields: { + readonly numberOfPackages: number | null; readonly insuredForAmount: string | null; readonly shipmentMethod: string | null; - readonly numberOfPackages: number | null; readonly number1: number | null; readonly number2: number | null; readonly remarks: string | null; @@ -5615,7 +5625,6 @@ export type Taxon = { readonly colStatus: string | null; readonly commonName: string | null; readonly cultivarName: string | null; - readonly environmentalProtectionStatus: string | null; readonly esaStatus: string | null; readonly fullName: string | null; readonly groupNumber: string | null; @@ -5639,6 +5648,7 @@ export type Taxon = { readonly number3: number | null; readonly number4: number | null; readonly number5: number | null; + readonly environmentalProtectionStatus: string | null; readonly rankId: number; readonly remarks: string | null; readonly source: string | null; @@ -6500,7 +6510,7 @@ export type CollectionObjectGroup = { readonly yesno2: boolean | null; readonly yesno3: boolean | null; }; - readonly toOneDependent: RR; + readonly toOneDependent: { readonly cojo: CollectionObjectGroupJoin | null }; readonly toOneIndependent: { readonly cogType: CollectionObjectGroupType; readonly collection: Collection | null; @@ -6508,7 +6518,9 @@ export type CollectionObjectGroup = { readonly modifiedByAgent: Agent | null; readonly parentCojo: CollectionObjectGroupJoin | null; }; - readonly toManyDependent: { readonly cojo: RA }; + readonly toManyDependent: { + readonly childCojos: RA; + }; readonly toManyIndependent: RR; }; export type CollectionObjectGroupJoin = { @@ -6575,10 +6587,10 @@ export type AbsoluteAge = { readonly remarks: string | null; readonly text1: string | null; readonly text2: string | null; - readonly yesno1: boolean | null; - readonly yesno2: boolean | null; readonly timestampCreated: string; readonly timestampModified: string | null; + readonly yesno1: boolean | null; + readonly yesno2: boolean | null; }; readonly toOneDependent: RR; readonly toOneIndependent: { @@ -6588,32 +6600,32 @@ export type AbsoluteAge = { readonly modifiedByAgent: Agent | null; }; readonly toManyDependent: { - readonly absoluteAgeAttachments: RA; + readonly absoluteAgeAttachments: RA; }; readonly toManyIndependent: RR; }; export type RelativeAge = { readonly tableName: 'RelativeAge'; readonly fields: { - readonly ageRype: string | null; + readonly ageType: string | null; readonly ageUncertainty: number | null; readonly collectionDate: string | null; readonly date1: string | null; readonly date2: string | null; readonly datingMethod: string | null; readonly datingMethodRemarks: string | null; - readonly relativeAgePeriod: number | null; readonly number1: number | null; readonly number2: number | null; + readonly relativeAgePeriod: number | null; readonly remarks: string | null; readonly text1: string | null; readonly text2: string | null; - readonly yesno1: boolean | null; - readonly yesno2: boolean | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly verbatimName: string | null; readonly verbatimPeriod: string | null; + readonly yesno1: boolean | null; + readonly yesno2: boolean | null; }; readonly toOneDependent: RR; readonly toOneIndependent: { @@ -6626,7 +6638,7 @@ export type RelativeAge = { readonly modifiedByAgent: Agent | null; }; readonly toManyDependent: { - readonly relativeAgeAttachments: RA; + readonly relativeAgeAttachments: RA; }; readonly toManyIndependent: RR; }; @@ -6639,9 +6651,7 @@ export type AbsoluteAgeAttachment = { readonly timestampModified: string | null; readonly version: number | null; }; - readonly toOneDependent: { - readonly attachment: Attachment; - }; + readonly toOneDependent: { readonly attachment: Attachment }; readonly toOneIndependent: { readonly absoluteAge: AbsoluteAge; readonly collectionMember: Collection; @@ -6660,9 +6670,7 @@ export type RelativeAgeAttachment = { readonly timestampModified: string | null; readonly version: number | null; }; - readonly toOneDependent: { - readonly attachment: Attachment; - }; + readonly toOneDependent: { readonly attachment: Attachment }; readonly toOneIndependent: { readonly collectionMember: Collection; readonly createdByAgent: Agent | null; @@ -6731,8 +6739,8 @@ export type TectonicUnitTreeDef = { readonly toOneDependent: RR; readonly toOneIndependent: { readonly createdByAgent: Agent | null; - readonly modifiedByAgent: Agent | null; readonly discipline: Discipline; + readonly modifiedByAgent: Agent | null; }; readonly toManyDependent: RR; readonly toManyIndependent: RR; @@ -6755,7 +6763,7 @@ export type TectonicUnitTreeDefItem = { }; readonly toOneDependent: RR; readonly toOneIndependent: { - readonly createdByAgent: Agent | null; + readonly createdbyagent: Agent | null; readonly modifiedByAgent: Agent | null; readonly parentItem: TectonicUnitTreeDefItem | null; readonly tectonicUnitTreeDef: TectonicUnitTreeDef; @@ -6763,7 +6771,7 @@ export type TectonicUnitTreeDefItem = { readonly toManyDependent: RR; readonly toManyIndependent: { readonly children: RA; - readonly treeEntries: RA; + readonly tectonicUnits: RA; }; }; export type TectonicUnit = { @@ -6792,7 +6800,7 @@ export type TectonicUnit = { readonly acceptedTectonicUnit: TectonicUnit | null; readonly createdByAgent: Agent | null; readonly modifiedByAgent: Agent | null; - readonly parent: TectonicUnit; + readonly parent: TectonicUnit | null; readonly tectonicUnitTreeDef: TectonicUnitTreeDef; readonly tectonicUnitTreeDefItem: TectonicUnitTreeDefItem; }; diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index 56dd8e72cc2..df0dd8ae210 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -575,7 +575,7 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List dirty: List[FieldChangeInfo] = [] for field_name, val in items: field = obj._meta.get_field(field_name) - if not field.many_to_one: continue + if not field.many_to_one and not field.one_to_one: continue old_related = get_related_or_none(obj, field_name) dependent = is_dependent_field(obj, field_name) diff --git a/specifyweb/specify/datamodel.py b/specifyweb/specify/datamodel.py index 16ca52942e4..ddb8c47c218 100644 --- a/specifyweb/specify/datamodel.py +++ b/specifyweb/specify/datamodel.py @@ -8294,8 +8294,9 @@ relationships=[ Relationship(name='collection', type='many-to-one', required=False, relatedModelName='Collection', column='CollectionID'), Relationship(name='cogType', type='many-to-one', required=True, relatedModelName='CollectionObjectGroupType', column='COGTypeID'), + Relationship(name='cojo', type='one-to-one', required=False, relatedModelName='CollectionObjectGroupJoin', otherSideName='childCog', dependent=True), Relationship(name='parentCojo', type='many-to-one', required=False, relatedModelName='CollectionObjectGroupJoin',column='CollectionObjectGroupJoinID', otherSideName='collectionobjectgroup'), - Relationship(name='cojo', type='one-to-many', required=False, relatedModelName='CollectionObjectGroupJoin', otherSideName='childCog', dependent=True), + Relationship(name='childCojos', type='one-to-many',required=False, dependent=True, relatedModelName='CollectionObjectGroupJoin', otherSideName='parentCog'), Relationship(name='createdByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='CreatedByAgentID'), Relationship(name='modifiedByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='ModifiedByAgentID'), ], @@ -8333,7 +8334,7 @@ ], relationships=[ - Relationship(name='parentCog', type='many-to-one', required=True, relatedModelName='CollectionObjectGroup', column='ParentCOGID', otherSideName='parentcojos'), + Relationship(name='parentCog', type='many-to-one', required=True, relatedModelName='CollectionObjectGroup', column='ParentCOGID', otherSideName='childCojos'), Relationship(name='childCog', type='one-to-one', required=False, relatedModelName='CollectionObjectGroup', column='ChildCOGID', otherSideName='cojo'), Relationship(name='childCo', type='one-to-one', required=False, relatedModelName='CollectionObject', column='ChildCOID', otherSideName='cojo'), Relationship(name='collectionobjectgroup', type='one-to-many',required=False, relatedModelName='CollectionObjectGroup', otherSideName='parentCojo'), diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index 96a40ab312c..c8e84d357ef 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -7604,7 +7604,7 @@ class Collectionobjectgroupjoin(models.Model): # aka. CoJo or CogJoin yesno3 = models.BooleanField(blank=True, null=True, unique=False, db_column='YesNo3', db_index=False) # Relationships: Many-to-One - parentcog = models.ForeignKey('CollectionObjectGroup', db_column='ParentCOGID', related_name='parentcojos', null=False, on_delete=models.CASCADE) + parentcog = models.ForeignKey('CollectionObjectGroup', db_column='ParentCOGID', related_name='childcojos', null=False, on_delete=models.CASCADE) # Relationships: One-to-One childcog = models.OneToOneField('CollectionObjectGroup', db_column='ChildCOGID', related_name='cojo', null=True, on_delete=models.CASCADE) From e19d2913b22e8d82c28e3c11e88374e2ce5551bb Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:11:33 -0700 Subject: [PATCH 103/132] Remove unecessary code --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 2562871d206..bafb579c068 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -80,9 +80,6 @@ export function COJODialog({ newCOJO.set('parentCog', parentResourceUrl as never); collection?.add(newCOJO); - - const parentResourceCojo = parentResource.getDependentResource('cojo'); - if (typeof parentResourceCojo === 'object') parentResourceCojo.add(newCOJO); }; const handleStates = (): void => { From fb3dc170c1b94cac890d09215750edd9ee0b16eb Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 17 Oct 2024 23:38:07 -0500 Subject: [PATCH 104/132] Propagate change from toMany relationship collections to related --- .../DataModel/__tests__/collectionApi.test.ts | 1 + .../DataModel/__tests__/resourceApi.test.ts | 85 +++++++++++++++++++ .../lib/components/DataModel/businessRules.ts | 1 + .../lib/components/DataModel/collectionApi.ts | 9 +- .../lib/components/DataModel/resourceApi.ts | 6 +- 5 files changed, 92 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index 3ea9b454cea..7caab498f0d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -293,6 +293,7 @@ describe('Independent Collection', () => { { id: 1, resource_uri: '/api/specify/collectionobject/1/', + collectionobjecttype: '/api/specify/collectionobjecttype/1/', text1: 'someValue', }, ]); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index ab6f0c8d895..de9ab69cf03 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -2,6 +2,7 @@ import { overrideAjax } from '../../../tests/ajax'; import { requireContext } from '../../../tests/helpers'; import type { RA } from '../../../utils/types'; import { replaceItem } from '../../../utils/utils'; +import { addMissingFields } from '../addMissingFields'; import type { SerializedRecord } from '../helperTypes'; import { getResourceApiUrl } from '../resource'; import { tables } from '../tables'; @@ -219,6 +220,90 @@ describe('rgetCollection', () => { // TEST: add dependent and independent tests for all relationship types (and zero-to-one) }); +describe('eventHandlerForToMany', () => { + test('saverequired', () => { + const resource = new tables.CollectionObject.Resource( + addMissingFields('CollectionObject', { + preparations: [ + { + id: 1, + _tableName: 'Preparation', + }, + ], + }) + ); + const testFunction = jest.fn(); + resource.on('saverequired', testFunction); + expect(testFunction).toHaveBeenCalledTimes(0); + expect(resource.needsSaved).toBe(false); + resource + .getDependentResource('preparations') + ?.models[0].set('text1', 'helloWorld'); + + expect(resource.needsSaved).toBe(true); + expect(testFunction).toHaveBeenCalledTimes(1); + }); + test('changing collection propagates to related', () => { + const resource = new tables.CollectionObject.Resource( + addMissingFields('CollectionObject', { + preparations: [ + { + id: 1, + _tableName: 'Preparation', + }, + ], + }) + ); + const onResourceChange = jest.fn(); + const onPrepChange = jest.fn(); + const onPrepAdd = jest.fn(); + const onPrepRemoval = jest.fn(); + resource.on('change', onResourceChange); + resource.on('change:preparations', onPrepChange); + resource.on('add:preparations', onPrepAdd); + resource.on('remove:preparations', onPrepRemoval); + + resource + .getDependentResource('preparations') + ?.models[0].set('text1', 'helloWorld', { silent: false }); + expect(onResourceChange).toHaveBeenCalledWith( + resource, + resource.getDependentResource('preparations') + ); + expect(onPrepChange).toHaveBeenCalledWith( + resource.getDependentResource('preparations')?.models[0], + { silent: false } + ); + const newPrep = new tables.Preparation.Resource({ + barCode: 'test', + }); + resource.getDependentResource('preparations')?.add(newPrep); + expect(onPrepAdd).toHaveBeenCalledWith( + newPrep, + resource.getDependentResource('preparations'), + {} + ); + resource.getDependentResource('preparations')?.remove(newPrep); + expect(onPrepRemoval).toHaveBeenCalledWith( + newPrep, + resource.getDependentResource('preparations'), + { index: 1 } + ); + + expect(onResourceChange).toHaveBeenCalledTimes(3); + + resource.set('determinations', [ + addMissingFields('Determination', { + taxon: getResourceApiUrl('Taxon', 1), + }), + ]); + expect(onResourceChange).toHaveBeenCalledTimes(4); + expect(onPrepChange).toHaveBeenCalledTimes(1); + expect(onPrepAdd).toHaveBeenCalledTimes(1); + expect(onPrepRemoval).toHaveBeenCalledTimes(1); + }); +}); + describe('needsSaved', () => { test('changing field makes needsSaved true', () => { const resource = new tables.CollectionObject.Resource({ diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts index 01c162c429e..1d674cda2a0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts @@ -56,6 +56,7 @@ export class BusinessRuleManager { if (isTreeResource(this.resource as SpecifyResource)) initializeTreeRecord(this.resource as SpecifyResource); + // REFACTOR: use the 'changed' event over 'change' this.resource.on('change', this.changed, this); this.resource.on('add', this.added, this); this.resource.on('remove', this.removed, this); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index 7effed034b9..b5e227d0f7d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -85,20 +85,15 @@ export const DependentCollection = Base.extend({ Base.call(this, records, options); }, initialize(_tables, options) { + setupToOne(this, options); this.on( 'add remove', function () { - /* - * Warning: changing a collection record does not trigger a - * change event in the parent (though it probably should) - */ this.trigger('saverequired'); }, this ); - setupToOne(this, options); - /* * If the id of the related resource changes, we go through and update * all the objects that point to it with the new pointer. @@ -243,7 +238,7 @@ export const IndependentCollection = LazyCollection.extend({ this ); - this.listenTo(options.related, 'saved', function () { + this.listenTo(this.related, 'saved', function () { this.updated = {}; this.removed = new Set(); }); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index f115ebfa3db..abd64f319cf 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -40,7 +40,6 @@ function eventHandlerForToOne(related, field) { switch (event) { case 'saverequired': { this.handleChanged(); - this.trigger.apply(this, args); return; } case 'change:id': { @@ -62,7 +61,7 @@ function eventHandlerForToOne(related, field) { }; } -function eventHandlerForToMany(_related, field) { +function eventHandlerForToMany(related, field) { return function (event) { const args = _.toArray(arguments); switch (event) { @@ -72,14 +71,15 @@ function eventHandlerForToMany(_related, field) { } case 'saverequired': { this.handleChanged(); - this.trigger.apply(this, args); break; } + case 'change': case 'add': case 'remove': { // Annotate add and remove events with the field in which they occurred args[0] = `${event}:${field.name.toLowerCase()}`; this.trigger.apply(this, args); + this.trigger.apply(this, ['change', this, related]); break; } } From 49d8307bc8e4873af029cdbd1c9275ce9851b3c1 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Fri, 18 Oct 2024 04:42:23 +0000 Subject: [PATCH 105/132] Lint code with ESLint and Prettier Triggered by fb3dc170c1b94cac890d09215750edd9ee0b16eb on branch refs/heads/issue-114-backend --- .../frontend/js_src/lib/components/DataModel/resourceApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index abd64f319cf..afa52be294c 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -79,7 +79,7 @@ function eventHandlerForToMany(related, field) { // Annotate add and remove events with the field in which they occurred args[0] = `${event}:${field.name.toLowerCase()}`; this.trigger.apply(this, args); - this.trigger.apply(this, ['change', this, related]); + Reflect.apply(this.trigger, this, ['change', this, related]); break; } } From a3e4d35c2a246bc7ddb1f6f7d3b6c63684108ec5 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 18 Oct 2024 06:59:35 -0700 Subject: [PATCH 106/132] Remove comment --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index bafb579c068..f106a46d405 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -56,7 +56,6 @@ export function COJODialog({ const handleCOJOCreation = ( selectedResource?: SpecifyResource ): void => { - // NewResource.set('cojo', newCOJO); Do this in bus rule when saving main COG? if (parentResource === undefined) return; const resourceToUse = selectedResource ?? newResource; From ee44f359bd59cdaed5a1c364aaa3aee240022a81 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Fri, 18 Oct 2024 07:17:33 -0700 Subject: [PATCH 107/132] Remove condition --- .../lib/components/FormCells/FormTable.tsx | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index 0102817ca2a..c9610c547c5 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -456,32 +456,31 @@ export function FormTable({ const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; - const addButtons = - isCOJO && typeof handleAddResources === 'function' ? ( - - } - /> - ) : typeof handleAddResources === 'function' && - mode !== 'view' && - !disableAdding ? ( - <> - {!isDependent && - hasTablePermission(relationship.relatedTable.name, 'read') ? ( - - ) : undefined} - {hasTablePermission(relationship.relatedTable.name, 'create') ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - }} - /> - ) : undefined} - - ) : undefined; + const addButtons = isCOJO ? ( + + } + /> + ) : typeof handleAddResources === 'function' && + mode !== 'view' && + !disableAdding ? ( + <> + {!isDependent && + hasTablePermission(relationship.relatedTable.name, 'read') ? ( + + ) : undefined} + {hasTablePermission(relationship.relatedTable.name, 'create') ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + }} + /> + ) : undefined} + + ) : undefined; return dialog === false ? ( From 44aa226669b2541fe5e86e59ddf414c0c9e4d14f Mon Sep 17 00:00:00 2001 From: Jason Melton <64045831+melton-jason@users.noreply.github.com> Date: Tue, 22 Oct 2024 04:54:47 +0000 Subject: [PATCH 108/132] Lint code with ESLint and Prettier Triggered by 7c458477ccd86190d9bb173ac040467be19fee0c on branch refs/heads/issue-114-backend --- .../InitialContext/__tests__/treeRanks.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts index 0d778d27776..1775ae0cb61 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts @@ -69,11 +69,11 @@ test('getTreeScope', () => expect( Object.fromEntries(testingTrees.map((tree) => [tree, getTreeScope(tree)])) ).toMatchInlineSnapshot(` - { - "Geography": "discipline", - "GeologicTimePeriod": "discipline", - "LithoStrat": "discipline", - "Storage": "institution", - "Taxon": "discipline", - } - `)); + { + "Geography": "discipline", + "GeologicTimePeriod": "discipline", + "LithoStrat": "discipline", + "Storage": "institution", + "Taxon": "discipline", + } + `)); From 075614d47f7352396f7bfab3550d7d5054e85d9f Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 21 Oct 2024 23:58:55 -0500 Subject: [PATCH 109/132] Improve collection fetching for Grid-based Subviews --- .../lib/components/DataModel/collectionApi.ts | 9 +++++---- .../FormCells/FormTableCollection.tsx | 11 ++++++++--- .../FormSliders/IntegratedRecordSelector.tsx | 6 ++++-- .../frontend/js_src/lib/hooks/useCollection.tsx | 17 ++++++++++++----- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index b5e227d0f7d..e05b3312bba 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -38,6 +38,7 @@ async function fakeFetch(rawOptions) { async function lazyFetch(options) { assert(this instanceof LazyCollection); + const self = this; if (this._fetch) return this._fetch; if (this.related?.isNew()) return fakeFetch.call(this, options); @@ -58,8 +59,8 @@ async function lazyFetch(options) { _(options).has('limit') && (options.data.limit = options.limit); this._fetch = Backbone.Collection.prototype.fetch.call(this, options); return this._fetch.then(() => { - this._fetch = null; - return this; + self._fetch = null; + return self; }); } @@ -216,7 +217,7 @@ export const IndependentCollection = LazyCollection.extend({ if (resource.isNew()) { this.updated[resource.cid] = resource; } else { - (this.removed as ReadonlySet).delete(resource.url()); + this.removed.delete(resource.url()); this.updated[resource.cid] = resource.url(); } this._totalCount += 1; @@ -229,7 +230,7 @@ export const IndependentCollection = LazyCollection.extend({ 'remove', function (resource: SpecifyResource) { if (!resource.isNew()) { - (this.removed as ReadonlySet).add(resource.url()); + this.removed.add(resource.url()); } this.updated = removeKey(this.updated, resource.cid); this._totalCount -= 1; diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx index 7602b17ac3b..5471c55eb86 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection } from '../DataModel/collectionApi'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; @@ -12,7 +13,7 @@ export function FormTableCollection({ collection, onAdd: handleAdd, onDelete: handleDelete, - onFetch: handleFetch, + onFetchMore: handleFetch, ...props }: Omit< Parameters[0], @@ -22,7 +23,9 @@ export function FormTableCollection({ readonly onDelete: | ((resource: SpecifyResource, index: number) => void) | undefined; - readonly onFetch?: () => void; + readonly onFetchMore?: ( + filters?: CollectionFetchFilters + ) => Promise | undefined>; }): JSX.Element | null { const [records, setRecords] = React.useState(Array.from(collection.models)); React.useEffect( @@ -37,7 +40,9 @@ export function FormTableCollection({ ); const handleFetchMore = React.useCallback(async () => { - handleFetch?.() ?? collection.fetch(); + await (typeof handleFetch === 'function' + ? handleFetch() + : collection.fetch()); setRecords(Array.from(collection.models)); }, [collection, handleFetch]); diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 3c541dc10c3..bb87436e140 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -56,7 +56,9 @@ export function IntegratedRecordSelector({ readonly viewName?: string; readonly urlParameter?: string; readonly onClose: () => void; - readonly onFetch?: (filters?: CollectionFetchFilters) => void; + readonly onFetch?: ( + filters?: CollectionFetchFilters + ) => Promise | undefined>; readonly sortField: SubViewSortField | undefined; }): JSX.Element { const containerRef = React.useRef(null); @@ -311,7 +313,7 @@ export function IntegratedRecordSelector({ if (isCollapsed) handleExpand(); handleDelete?.(index, 'minusButton'); }} - onFetch={handleFetch} + onFetchMore={handleFetch} /> ) : null} {dialogs} diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 638d4ad55df..53f1b900ee1 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -8,7 +8,7 @@ import type { Collection } from '../components/DataModel/specifyTable'; import { raise } from '../components/Errors/Crash'; import type { SubViewSortField } from '../components/FormParse/cells'; import { relationshipIsToMany } from '../components/WbPlanView/mappingHelpers'; -import type { GetOrSet } from '../utils/types'; +import type { GetOrSet, RA } from '../utils/types'; import { overwriteReadOnly } from '../utils/types'; import { sortFunction } from '../utils/utils'; import { useAsyncState } from './useAsyncState'; @@ -25,7 +25,9 @@ export function useCollection({ sortBy, }: UseCollectionProps): readonly [ ...GetOrSet | false | undefined>, - (filters?: CollectionFetchFilters) => void + ( + filters?: CollectionFetchFilters + ) => Promise | undefined> ] { const [collection, setCollection] = useAsyncState< Collection | false | undefined @@ -52,13 +54,15 @@ export function useCollection({ const versionRef = React.useRef(0); const handleFetch = React.useCallback( - (filters?: CollectionFetchFilters): void => { + async ( + filters?: CollectionFetchFilters + ): Promise | undefined> => { if (typeof collection !== 'object') return undefined; versionRef.current += 1; const localVersionRef = versionRef.current; - collection + return collection .fetch({ ...filters, success: (collection) => { @@ -71,7 +75,10 @@ export function useCollection({ setCollection(collection); }, } as CollectionFetchFilters) - .catch(raise); + .catch((error: Error, ...args: RA) => { + raise(error, args); + return undefined; + }); }, [collection, setCollection] ); From 96d39939a4431208f27f80fcfab5e36087720f5b Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 22 Oct 2024 06:23:58 -0700 Subject: [PATCH 110/132] Add field check for childCOJOs --- .../lib/components/FormCells/COJODialog.tsx | 22 ++++---- .../lib/components/FormCells/FormTable.tsx | 52 ++++++++++--------- .../FormSliders/IntegratedRecordSelector.tsx | 3 +- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index f106a46d405..5cf5fd6c2f4 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -33,7 +33,7 @@ export function COJODialog({ const [state, setState] = React.useState<'Add' | 'Search' | undefined>( undefined ); - const [resource, setResource] = React.useState< + const [resourceTable, setResourceTable] = React.useState< | SpecifyTable | SpecifyTable | undefined @@ -45,13 +45,13 @@ export function COJODialog({ >(undefined); React.useEffect(() => { - if (resource !== undefined) { - const createdResource = new resource.Resource() as + if (resourceTable !== undefined) { + const createdResource = new resourceTable.Resource() as | SpecifyResource | SpecifyResource; setNewResource(createdResource); } - }, [resource]); + }, [resourceTable]); const handleCOJOCreation = ( selectedResource?: SpecifyResource @@ -62,9 +62,7 @@ export function COJODialog({ if (resourceToUse === undefined) return; - if (newResource) { - void newResource.save(); - } + void newResource?.save(); const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); const field = @@ -83,7 +81,7 @@ export function COJODialog({ const handleStates = (): void => { setState(undefined); - setResource(undefined); + setResourceTable(undefined); handleClose(); }; @@ -101,18 +99,18 @@ export function COJODialog({ {COJOChildrenTables.map((table) => (
- {localized(table.label)} + {table.label} { setState('Add'); - setResource(table); + setResourceTable(table); }} /> { setState('Search'); - setResource(table); + setResourceTable(table); }} />
@@ -147,7 +145,7 @@ export function COJODialog({ forceCollection={undefined} multiple searchView={undefined} - table={resource as SpecifyTable} + table={resourceTable as SpecifyTable} onClose={(): void => setState(undefined)} onSelected={(selectedResources): void => { selectedResources.forEach((selectedResource) => { diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index c9610c547c5..82f78bfc361 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -455,32 +455,34 @@ export function FormTable({ ); const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + const isChildCojos = relationship.name === 'childCojos'; - const addButtons = isCOJO ? ( - - } - /> - ) : typeof handleAddResources === 'function' && - mode !== 'view' && - !disableAdding ? ( - <> - {!isDependent && - hasTablePermission(relationship.relatedTable.name, 'read') ? ( - - ) : undefined} - {hasTablePermission(relationship.relatedTable.name, 'create') ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - }} - /> - ) : undefined} - - ) : undefined; + const addButtons = + isCOJO && isChildCojos ? ( + + } + /> + ) : typeof handleAddResources === 'function' && + mode !== 'view' && + !disableAdding ? ( + <> + {!isDependent && + hasTablePermission(relationship.relatedTable.name, 'read') ? ( + + ) : undefined} + {hasTablePermission(relationship.relatedTable.name, 'create') ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + }} + /> + ) : undefined} + + ) : undefined; return dialog === false ? ( diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 2b77e767da1..f2c1a954884 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -134,6 +134,7 @@ export function IntegratedRecordSelector({ collection.table.specifyTable.name.includes('Attachment'); const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + const isChildCojos = relationship.name === 'childCojos'; return ( @@ -219,7 +220,7 @@ export function IntegratedRecordSelector({ relationship.relatedTable.name, 'create' ) && typeof handleAdd === 'function' ? ( - isCOJO ? ( + isCOJO && isChildCojos ? ( Date: Tue, 22 Oct 2024 06:24:52 -0700 Subject: [PATCH 111/132] Add to do --- .../frontend/js_src/lib/components/FormCells/FormTable.tsx | 1 + .../lib/components/FormSliders/IntegratedRecordSelector.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index 82f78bfc361..40bc24ea3ff 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -455,6 +455,7 @@ export function FormTable({ ); const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + // TODO: change when upadte childCojos to children in models const isChildCojos = relationship.name === 'childCojos'; const addButtons = diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index f2c1a954884..3c6882476f0 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -134,6 +134,7 @@ export function IntegratedRecordSelector({ collection.table.specifyTable.name.includes('Attachment'); const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; + // TODO: change when upadte childCojos to children in models const isChildCojos = relationship.name === 'childCojos'; return ( From 7faa4ffab6489e80fccb983916b836333ea02b0d Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 23 Oct 2024 05:55:03 -0700 Subject: [PATCH 112/132] Test setting parentCojo --- .../js_src/lib/components/FormCells/COJODialog.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 5cf5fd6c2f4..21ec84d25a6 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -54,7 +54,9 @@ export function COJODialog({ }, [resourceTable]); const handleCOJOCreation = ( - selectedResource?: SpecifyResource + selectedResource?: + | SpecifyResource + | SpecifyResource ): void => { if (parentResource === undefined) return; @@ -76,6 +78,13 @@ export function COJODialog({ newCOJO.set(field, resourceUrl as never); newCOJO.set('parentCog', parentResourceUrl as never); + if (resourceToUse.specifyTable.name === 'CollectionObjectGroup') { + (resourceToUse as SpecifyResource).set( + 'parentCojo', + newCOJO + ); + } + collection?.add(newCOJO); }; From c6c9d8c38e625efada67fb14159e7eb20cd40588 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 23 Oct 2024 12:00:08 -0500 Subject: [PATCH 113/132] Allow passing filters to resource.rgetCollection --- .../lib/components/DataModel/legacyTypes.ts | 4 +- .../lib/components/DataModel/resourceApi.ts | 27 +++-- .../js_src/lib/components/Forms/SubView.tsx | 10 +- .../js_src/lib/hooks/useCollection.tsx | 106 ++++++++---------- 4 files changed, 72 insertions(+), 75 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts index 46b65d3b3df..5386c463198 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts @@ -4,6 +4,7 @@ import type { IR, RA } from '../../utils/types'; import type { BusinessRuleManager } from './businessRules'; +import { CollectionFetchFilters } from './collection'; import type { AnySchema, CommonFields, @@ -113,7 +114,8 @@ export type SpecifyResource = { VALUE extends (SCHEMA['toManyDependent'] & SCHEMA['toManyIndependent'])[FIELD_NAME] >( - fieldName: FIELD_NAME + fieldName: FIELD_NAME, + filters?: CollectionFetchFilters ): Promise>; set< FIELD_NAME extends diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index afa52be294c..a3a6f106d25 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -10,6 +10,7 @@ import { softFail } from '../Errors/Crash'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; +import { CollectionFetchFilters } from './collection'; import { isRelationshipCollection } from './collectionApi'; import { backboneFieldSeparator } from './helpers'; import type { @@ -606,8 +607,12 @@ export const ResourceBase = Backbone.Model.extend({ ); }, // Duplicate definition for purposes of better typing: - async rgetCollection(fieldName) { - return this.getRelated(fieldName, { prePop: true }); + async rgetCollection(fieldName, rawOptions) { + const options = { + ...rawOptions, + prePop: true, + }; + return this.getRelated(fieldName, options); }, async getRelated(fieldName, options) { options ||= { @@ -695,8 +700,7 @@ export const ResourceBase = Backbone.Model.extend({ console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); } else { - const fetchedToOne = toOne.isNew() ? toOne : toOne; - this.storeIndependent(field, fetchedToOne); + this.storeIndependent(field, toOne); } } // If we want a field within the related resource then recur @@ -708,8 +712,8 @@ export const ResourceBase = Backbone.Model.extend({ } return field.isDependent() - ? this.getDependentToMany(field) - : this.getIndependentToMany(field); + ? this.getDependentToMany(field, options) + : this.getIndependentToMany(field, options); } case 'zero-to-one': { /* @@ -752,7 +756,8 @@ export const ResourceBase = Backbone.Model.extend({ } }, async getDependentToMany( - field: Relationship + field: Relationship, + filters ): Promise> { assert(field.isDependent()); @@ -776,7 +781,7 @@ export const ResourceBase = Backbone.Model.extend({ ? this.isNew() ? new relatedTable.DependentCollection(collectionOptions, []) : await new relatedTable.ToOneCollection(collectionOptions) - .fetch({ limit: 0 }) + .fetch({ ...filters, limit: 0 }) .then( (collection) => new relatedTable.DependentCollection( @@ -786,13 +791,14 @@ export const ResourceBase = Backbone.Model.extend({ ) : existingToMany; - return collection.fetch({ limit: 0 }).then((collection) => { + return collection.fetch({ ...filters, limit: 0 }).then((collection) => { self.storeDependent(field, collection); return collection; }); }, async getIndependentToMany( - field: Relationship + field: Relationship, + filters ): Promise> { assert(!field.isDependent()); @@ -813,6 +819,7 @@ export const ResourceBase = Backbone.Model.extend({ : existingToMany; return collection.fetch({ + ...filters, // Only store the collection if fetch is successful (doesn't return undefined) success: (collection) => { this.storeIndependent(field, collection); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 54b674005e4..67de74bc85c 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -79,12 +79,10 @@ export function SubView({ parentResource, 'saved', (): void => { - if (!relationship.isDependent()) { - handleFetch({ - offset: 0, - reset: true, - } as CollectionFetchFilters); - } + handleFetch({ + offset: 0, + reset: true, + } as CollectionFetchFilters); }, false ), diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 53f1b900ee1..0d9e28e6bb4 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -5,10 +5,9 @@ import type { AnySchema } from '../components/DataModel/helperTypes'; import type { SpecifyResource } from '../components/DataModel/legacyTypes'; import type { Relationship } from '../components/DataModel/specifyField'; import type { Collection } from '../components/DataModel/specifyTable'; -import { raise } from '../components/Errors/Crash'; import type { SubViewSortField } from '../components/FormParse/cells'; import { relationshipIsToMany } from '../components/WbPlanView/mappingHelpers'; -import type { GetOrSet, RA } from '../utils/types'; +import type { GetOrSet } from '../utils/types'; import { overwriteReadOnly } from '../utils/types'; import { sortFunction } from '../utils/utils'; import { useAsyncState } from './useAsyncState'; @@ -17,6 +16,7 @@ type UseCollectionProps = { readonly parentResource: SpecifyResource; readonly relationship: Relationship; readonly sortBy?: SubViewSortField; + readonly filters?: CollectionFetchFilters; }; export function useCollection({ @@ -44,7 +44,6 @@ export function useCollection({ : fetchToOneCollection({ parentResource, relationship, - sortBy, }), [sortBy, parentResource, relationship] ), @@ -62,25 +61,28 @@ export function useCollection({ versionRef.current += 1; const localVersionRef = versionRef.current; - return collection - .fetch({ - ...filters, - success: (collection) => { - /* - * If the collection is already being fetched, don't update it - * to prevent a race condition. - * REFACTOR: simplify this - */ - if (versionRef.current === localVersionRef) - setCollection(collection); - }, - } as CollectionFetchFilters) - .catch((error: Error, ...args: RA) => { - raise(error, args); - return undefined; - }); + const fetchCollection = + relationshipIsToMany(relationship) && + relationship.type !== 'zero-to-one' + ? fetchToManyCollection({ + parentResource, + relationship, + sortBy, + filters, + }) + : fetchToOneCollection({ parentResource, relationship }); + + return fetchCollection.then((collection) => { + if ( + typeof collection === 'object' && + versionRef.current === localVersionRef + ) { + setCollection(collection); + } + return collection === false ? undefined : collection; + }); }, - [collection, setCollection] + [collection, parentResource, relationship, setCollection, sortBy] ); return [collection, setCollection, handleFetch]; } @@ -89,37 +91,39 @@ const fetchToManyCollection = async ({ parentResource, relationship, sortBy, + filters, }: UseCollectionProps): Promise | undefined> => - parentResource.rgetCollection(relationship.name).then((collection) => { - // TEST: check if this can ever happen - if (collection === null || collection === undefined) - return new relationship.relatedTable.DependentCollection({ - related: parentResource, - field: relationship.getReverse(), - }) as Collection; - if (sortBy === undefined) return collection; + parentResource + .rgetCollection(relationship.name, filters) + .then((collection) => { + // TEST: check if this can ever happen + if (collection === null || collection === undefined) + return new relationship.relatedTable.DependentCollection({ + related: parentResource, + field: relationship.getReverse(), + }) as Collection; + if (sortBy === undefined) return collection; - // BUG: this does not look into related tables - const field = sortBy.fieldNames[0]; + // BUG: this does not look into related tables + const field = sortBy.fieldNames[0]; - // Overwriting the models on the collection - overwriteReadOnly( - collection, - 'models', - Array.from(collection.models).sort( - sortFunction( - (resource) => resource.get(field), - sortBy.direction === 'desc' + // Overwriting the models on the collection + overwriteReadOnly( + collection, + 'models', + Array.from(collection.models).sort( + sortFunction( + (resource) => resource.get(field), + sortBy.direction === 'desc' + ) ) - ) - ); - return collection; - }); + ); + return collection; + }); async function fetchToOneCollection({ parentResource, relationship, - sortBy, }: UseCollectionProps): Promise< Collection | false | undefined > { @@ -160,19 +164,5 @@ async function fetchToOneCollection({ 'field', collection.field ?? relationship.getReverse() ); - if (sortBy !== undefined) { - // BUG: this does not look into related tables - const field = sortBy.fieldNames[0]; - overwriteReadOnly( - collection, - 'models', - Array.from(collection.models).sort( - sortFunction( - (resource) => resource.get(field), - sortBy.direction === 'desc' - ) - ) - ); - } return collection; } From 81b8751fd7c90a3680205d374876c9bb8510723d Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 23 Oct 2024 17:04:05 +0000 Subject: [PATCH 114/132] Lint code with ESLint and Prettier Triggered by c6c9d8c38e625efada67fb14159e7eb20cd40588 on branch refs/heads/issue-114-backend --- .../frontend/js_src/lib/components/DataModel/legacyTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts index 5386c463198..13bb62f3826 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts @@ -4,7 +4,7 @@ import type { IR, RA } from '../../utils/types'; import type { BusinessRuleManager } from './businessRules'; -import { CollectionFetchFilters } from './collection'; +import type { CollectionFetchFilters } from './collection'; import type { AnySchema, CommonFields, @@ -158,7 +158,7 @@ export type SpecifyResource = { ): SpecifyResource; // Not type safe bulkSet(value: IR): SpecifyResource; - //Unsafe + // Unsafe readonly independentResources: IR< Collection | SpecifyResource | null | undefined >; From f53e48d62f469eb598c984b5877a22ba517af6f4 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Wed, 23 Oct 2024 12:43:52 -0500 Subject: [PATCH 115/132] Update comment in fetchToOneCollection --- specifyweb/frontend/js_src/lib/hooks/useCollection.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index 0d9e28e6bb4..b9b95b00772 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -129,10 +129,8 @@ async function fetchToOneCollection({ > { /** * If relationship is -to-one, create a collection for the related - * resource. This allows to reuse most of the code from the -to-many - * relationships. RecordSelector handles collections with -to-one - * related field by removing the "+" button after first record is added - * and not rendering record count or record slider. + * resource. This allows to reuse most of the code from -to-many + * relationships in components like Subview and RecordSelectorFromCollection */ const resource = await parentResource.rgetPromise(relationship.name); const reverse = relationship.getReverse(); From a97855aff508209f42341556007c0c58e8938217 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 25 Oct 2024 11:27:40 -0400 Subject: [PATCH 116/132] Add cojo back to COG --- specifyweb/frontend/js_src/lib/components/DataModel/types.ts | 1 + specifyweb/specify/datamodel.py | 1 + 2 files changed, 2 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index 25cb57137f9..bbe15086419 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -6522,6 +6522,7 @@ export type CollectionObjectGroup = { }; readonly toManyDependent: { readonly children: RA; + readonly cojo: RA; }; readonly toManyIndependent: RR; }; diff --git a/specifyweb/specify/datamodel.py b/specifyweb/specify/datamodel.py index b445e89b28a..366d6b76385 100644 --- a/specifyweb/specify/datamodel.py +++ b/specifyweb/specify/datamodel.py @@ -8297,6 +8297,7 @@ Relationship(name='collection', type='many-to-one', required=False, relatedModelName='Collection', column='CollectionID'), Relationship(name='cogType', type='many-to-one', required=True, relatedModelName='CollectionObjectGroupType', column='COGTypeID'), Relationship(name='parentCojo', type='many-to-one', required=False, relatedModelName='CollectionObjectGroupJoin',column='CollectionObjectGroupJoinID', otherSideName='collectionobjectgroup'), + Relationship(name='cojo', type='one-to-many', required=False, relatedModelName='CollectionObjectGroupJoin', otherSideName='childCog', dependent=True), Relationship(name='children', type='one-to-many', required=False, dependent=True, relatedModelName='CollectionObjectGroupJoin', otherSideName='parentCog'), Relationship(name='createdByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='CreatedByAgentID'), Relationship(name='modifiedByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='ModifiedByAgentID'), From 5a94c4ffba19d87d1d6b038d12b0a3b19d73e957 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 25 Oct 2024 11:52:14 -0400 Subject: [PATCH 117/132] Use children relationship for opening COJO dialog --- .../lib/components/FormCells/FormTable.tsx | 57 +++++++++---------- .../FormSliders/IntegratedRecordSelector.tsx | 8 +-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index 40bc24ea3ff..8d59431d024 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -454,36 +454,35 @@ export function FormTable({
); - const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; - // TODO: change when upadte childCojos to children in models - const isChildCojos = relationship.name === 'childCojos'; + const isCOJO = + relationship.relatedTable.name === 'CollectionObjectGroupJoin' && + relationship.name === 'children'; - const addButtons = - isCOJO && isChildCojos ? ( - - } - /> - ) : typeof handleAddResources === 'function' && - mode !== 'view' && - !disableAdding ? ( - <> - {!isDependent && - hasTablePermission(relationship.relatedTable.name, 'read') ? ( - - ) : undefined} - {hasTablePermission(relationship.relatedTable.name, 'create') ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - }} - /> - ) : undefined} - - ) : undefined; + const addButtons = isCOJO ? ( + + } + /> + ) : typeof handleAddResources === 'function' && + mode !== 'view' && + !disableAdding ? ( + <> + {!isDependent && + hasTablePermission(relationship.relatedTable.name, 'read') ? ( + + ) : undefined} + {hasTablePermission(relationship.relatedTable.name, 'create') ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + }} + /> + ) : undefined} + + ) : undefined; return dialog === false ? ( diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index 63c29d1d65a..fe31b2fe546 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -135,9 +135,9 @@ export function IntegratedRecordSelector({ const isAttachmentTable = collection.table.specifyTable.name.includes('Attachment'); - const isCOJO = relationship.relatedTable.name === 'CollectionObjectGroupJoin'; - // TODO: change when upadte childCojos to children in models - const isChildCojos = relationship.name === 'childCojos'; + const isCOJO = + relationship.relatedTable.name === 'CollectionObjectGroupJoin' && + relationship.name === 'children'; return ( @@ -223,7 +223,7 @@ export function IntegratedRecordSelector({ relationship.relatedTable.name, 'create' ) && typeof handleAdd === 'function' ? ( - isCOJO && isChildCojos ? ( + isCOJO ? ( Date: Fri, 25 Oct 2024 12:22:35 -0400 Subject: [PATCH 118/132] Remove unused import --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index 21ec84d25a6..bd96517f493 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; -import { localized } from '../../utils/types'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; From 6c220a4a76f24c50b5bee94a23d8c71ba3471b84 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 25 Oct 2024 15:25:52 -0400 Subject: [PATCH 119/132] Save parentCojo on childCog --- specifyweb/businessrules/rules/cojo_rules.py | 9 +++++++-- .../js_src/lib/components/FormCells/COJODialog.tsx | 8 -------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index d4925fa5932..574cb3a5623 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -1,6 +1,6 @@ from specifyweb.businessrules.exceptions import BusinessRuleException from specifyweb.businessrules.orm_signal_handler import orm_signal_handler -from specifyweb.specify.models import Collectionobjectgroupjoin +from specifyweb.specify.models import Collectionobjectgroupjoin, Collectionobjectgroup @orm_signal_handler('pre_save', 'Collectionobjectgroupjoin') def cojo_pre_save(cojo): @@ -24,4 +24,9 @@ def cojo_pre_save(cojo): (Collectionobjectgroupjoin.objects .filter(parentcog=cojo.parentcog) .update(issubstrate=False)) - \ No newline at end of file + +@orm_signal_handler('post_save', 'Collectionobjectgroupjoin') +def cojo_post_save(cojo): + if cojo.childcog is not None: + cojo.childcog.parentcojo = cojo + cojo.childcog.save() \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index bd96517f493..f224be0ec71 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -76,14 +76,6 @@ export function COJODialog({ newCOJO.set(field, resourceUrl as never); newCOJO.set('parentCog', parentResourceUrl as never); - - if (resourceToUse.specifyTable.name === 'CollectionObjectGroup') { - (resourceToUse as SpecifyResource).set( - 'parentCojo', - newCOJO - ); - } - collection?.add(newCOJO); }; From 40d55c1330cdc708156cf147fb5a92f9c4626850 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 25 Oct 2024 16:06:48 -0400 Subject: [PATCH 120/132] Remove unused import --- specifyweb/businessrules/rules/cojo_rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index 574cb3a5623..8633d0f666e 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -1,6 +1,6 @@ from specifyweb.businessrules.exceptions import BusinessRuleException from specifyweb.businessrules.orm_signal_handler import orm_signal_handler -from specifyweb.specify.models import Collectionobjectgroupjoin, Collectionobjectgroup +from specifyweb.specify.models import Collectionobjectgroupjoin @orm_signal_handler('pre_save', 'Collectionobjectgroupjoin') def cojo_pre_save(cojo): From 301e676eba0c7e71f8ba7467b1247a3cd882ca86 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 28 Oct 2024 07:53:22 -0700 Subject: [PATCH 121/132] Chnage resource to save --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index f224be0ec71..dbd0bec5e2a 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -63,7 +63,7 @@ export function COJODialog({ if (resourceToUse === undefined) return; - void newResource?.save(); + void resourceToUse?.save(); const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); const field = From 3a121c412c4b2bb12ba6d4a98fd13e28eaea8e98 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:26:56 -0700 Subject: [PATCH 122/132] Remove unecessary code --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index dbd0bec5e2a..df519599411 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -6,6 +6,7 @@ import { formsText } from '../../localization/forms'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { fetchResource } from '../DataModel/resource'; import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; import type { @@ -63,8 +64,6 @@ export function COJODialog({ if (resourceToUse === undefined) return; - void resourceToUse?.save(); - const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); const field = resourceToUse.specifyTable.name === 'CollectionObject' From 4ad6de624a4baac96c792acde16f062ba060dd55 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:31:27 -0700 Subject: [PATCH 123/132] Remove import --- .../frontend/js_src/lib/components/FormCells/COJODialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx index df519599411..0f2e9a347dd 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx @@ -6,7 +6,6 @@ import { formsText } from '../../localization/forms'; import { DataEntry } from '../Atoms/DataEntry'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; -import { fetchResource } from '../DataModel/resource'; import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; import type { From b11972eb758a4771f3746e66c98d9e3b2c8d4b60 Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:51:33 -0700 Subject: [PATCH 124/132] Test bus rule on delte cojo for parent --- specifyweb/businessrules/rules/cojo_rules.py | 7 ++++++- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index 8633d0f666e..d98ce0310b7 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -29,4 +29,9 @@ def cojo_pre_save(cojo): def cojo_post_save(cojo): if cojo.childcog is not None: cojo.childcog.parentcojo = cojo - cojo.childcog.save() \ No newline at end of file + cojo.childcog.save() + +@orm_signal_handler('pre_delete', 'Collectionobjectgroupjoin') +def cojo_pre_delete(cojo): + cojo.childcog.parentcojo = None + cojo.childcog.save() \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 5f32c1f55c9..3c58b79a5a0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -142,7 +142,6 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }, }, }, - CollectionObject: { customInit: (collectionObject: SpecifyResource): void => { const ceField = collectionObject.specifyTable.getField('collectingEvent'); From 4e94695c4b74adf410e00989947d6c2d95f10457 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 30 Oct 2024 14:08:16 -0400 Subject: [PATCH 125/132] update types --- .../js_src/lib/components/DataModel/types.ts | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index 80c435dbe70..fd6c1b62bcc 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -242,20 +242,20 @@ export type Tables = { export type Accession = { readonly tableName: 'Accession'; readonly fields: { - readonly accessionCondition: string | null; readonly accessionNumber: string; + readonly accessionCondition: string | null; + readonly dateAccessioned: string | null; readonly actualTotalCountAmt: number | null; readonly collectionObjectCount: number | null; - readonly dateAccessioned: string | null; readonly dateAcknowledged: string | null; - readonly dateReceived: string | null; + readonly remarks: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; readonly number1: number | null; readonly number2: number | null; readonly preparationCount: number | null; - readonly remarks: string | null; + readonly dateReceived: string | null; readonly status: string | null; readonly text1: string | null; readonly text2: string | null; @@ -461,10 +461,10 @@ export type Agent = { readonly initials: string | null; readonly integer1: number | null; readonly integer2: number | null; - readonly interests: string | null; readonly jobTitle: string | null; readonly lastName: string | null; readonly middleInitial: string | null; + readonly interests: string | null; readonly remarks: string | null; readonly suffix: string | null; readonly text1: string | null; @@ -497,8 +497,8 @@ export type Agent = { readonly agentAttachments: RA; readonly agentGeographies: RA; readonly agentSpecialties: RA; - readonly groups: RA; readonly identifiers: RA; + readonly groups: RA; readonly variants: RA; }; readonly toManyIndependent: { @@ -646,6 +646,7 @@ export type Attachment = { readonly fields: { readonly attachmentLocation: string | null; readonly attachmentStorageConfig: string | null; + readonly captureDevice: string | null; readonly copyrightDate: string | null; readonly copyrightHolder: string | null; readonly credit: string | null; @@ -662,7 +663,6 @@ export type Attachment = { readonly scopeID: number | null; readonly scopeType: number | null; readonly subjectOrientation: string | null; - readonly captureDevice: string | null; readonly subtype: string | null; readonly tableID: number | null; readonly timestampCreated: string; @@ -929,13 +929,13 @@ export type BorrowMaterial = { readonly tableName: 'BorrowMaterial'; readonly fields: { readonly collectionMemberId: number; - readonly description: string | null; readonly inComments: string | null; readonly materialNumber: string; readonly outComments: string | null; readonly quantity: number | null; readonly quantityResolved: number | null; readonly quantityReturned: number | null; + readonly description: string | null; readonly text1: string | null; readonly text2: string | null; readonly timestampCreated: string; @@ -982,6 +982,8 @@ export type CollectingEvent = { readonly endDatePrecision: number | null; readonly endDateVerbatim: string | null; readonly endTime: number | null; + readonly stationFieldNumber: string | null; + readonly method: string | null; readonly guid: string | null; readonly integer1: number | null; readonly integer2: number | null; @@ -991,8 +993,8 @@ export type CollectingEvent = { readonly reservedText1: string | null; readonly reservedText2: string | null; readonly sgrStatus: number | null; - readonly startDate: string | null; readonly startDatePrecision: number | null; + readonly startDateVerbatim: string | null; readonly startTime: number | null; readonly stationFieldNumberModifier1: string | null; readonly stationFieldNumberModifier2: string | null; @@ -1005,6 +1007,8 @@ export type CollectingEvent = { readonly text6: string | null; readonly text7: string | null; readonly text8: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly uniqueIdentifier: string | null; readonly verbatimDate: string | null; readonly verbatimLocality: string | null; @@ -1018,8 +1022,8 @@ export type CollectingEvent = { readonly collectingTrip: CollectingTrip | null; readonly createdByAgent: Agent | null; readonly discipline: Discipline; - readonly modifiedByAgent: Agent | null; readonly locality: Locality | null; + readonly modifiedByAgent: Agent | null; readonly paleoContext: PaleoContext | null; readonly visibilitySetBy: SpecifyUser | null; }; @@ -1089,6 +1093,8 @@ export type CollectingEventAttribute = { readonly integer7: number | null; readonly integer8: number | null; readonly integer9: number | null; + readonly number12: number | null; + readonly number13: number | null; readonly number1: number | null; readonly number10: number | null; readonly number11: number | null; @@ -1100,8 +1106,6 @@ export type CollectingEventAttribute = { readonly number7: number | null; readonly number8: number | null; readonly number9: number | null; - readonly number3: number | null; - readonly text3: string | null; readonly remarks: string | null; readonly text6: string | null; readonly text1: string | null; @@ -1129,8 +1133,8 @@ export type CollectingEventAttribute = { readonly toOneIndependent: { readonly createdByAgent: Agent | null; readonly discipline: Discipline; - readonly modifiedByAgent: Agent | null; readonly hostTaxon: Taxon | null; + readonly modifiedByAgent: Agent | null; }; readonly toManyDependent: RR; readonly toManyIndependent: { @@ -1159,6 +1163,7 @@ export type CollectingTrip = { readonly tableName: 'CollectingTrip'; readonly fields: { readonly cruise: string | null; + readonly text2: string | null; readonly date1: string | null; readonly date1Precision: number | null; readonly date2: string | null; @@ -1176,8 +1181,6 @@ export type CollectingTrip = { readonly startDatePrecision: number | null; readonly startDateVerbatim: string | null; readonly startTime: number | null; - readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -1189,6 +1192,7 @@ export type CollectingTrip = { readonly timestampModified: string | null; readonly collectingTripName: string | null; readonly version: number | null; + readonly text1: string | null; readonly vessel: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; @@ -1364,9 +1368,10 @@ export type Collection = { export type CollectionObject = { readonly tableName: 'CollectionObject'; readonly fields: { - readonly yesNo1: boolean | null; readonly actualTotalCountAmt: number | null; + readonly age: number | null; readonly availability: string | null; + readonly catalogNumber: string | null; readonly catalogedDate: string | null; readonly catalogedDatePrecision: number | null; readonly catalogedDateVerbatim: string | null; @@ -1382,10 +1387,10 @@ export type CollectionObject = { readonly embargoReleaseDatePrecision: number | null; readonly embargoStartDate: string | null; readonly embargoStartDatePrecision: number | null; - readonly fieldNumber: string | null; readonly guid: string | null; readonly integer1: number | null; readonly integer2: number | null; + readonly text2: string | null; readonly inventoryDate: string | null; readonly inventoryDatePrecision: number | null; readonly modifier: string | null; @@ -1398,22 +1403,22 @@ export type CollectionObject = { readonly ocr: string | null; readonly altCatalogNumber: string | null; readonly projectNumber: string | null; + readonly remarks: string | null; readonly reservedInteger3: number | null; readonly reservedInteger4: number | null; - readonly reservedText: string | null; readonly reservedText2: string | null; readonly reservedText3: string | null; readonly restrictions: string | null; readonly sgrStatus: number | null; + readonly text1: string | null; readonly description: string | null; - readonly catalogNumber: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; readonly text6: string | null; readonly text7: string | null; readonly text8: string | null; + readonly timestampCreated: string; readonly totalCountAmt: number | null; readonly totalValue: number | null; readonly uniqueIdentifier: string | null; @@ -1435,17 +1440,17 @@ export type CollectionObject = { readonly agent1: Agent | null; readonly appraisal: Appraisal | null; readonly cataloger: Agent | null; - readonly collectingEvent: CollectingEvent | null; readonly collection: Collection; readonly collectionObjectType: CollectionObjectType; readonly container: Container | null; readonly containerOwner: Container | null; readonly createdByAgent: Agent | null; readonly currentDetermination: Determination | null; + readonly modifiedByAgent: Agent | null; readonly embargoAuthority: Agent | null; + readonly collectingEvent: CollectingEvent | null; readonly fieldNotebookPage: FieldNotebookPage | null; readonly inventorizedBy: Agent | null; - readonly modifiedByAgent: Agent | null; readonly paleoContext: PaleoContext | null; readonly visibilitySetBy: SpecifyUser | null; }; @@ -1533,8 +1538,6 @@ export type CollectionObjectAttribute = { readonly number1: number | null; readonly number10: number | null; readonly number11: number | null; - readonly number12: number | null; - readonly number13: number | null; readonly number14: number | null; readonly number15: number | null; readonly number16: number | null; @@ -1576,6 +1579,7 @@ export type CollectionObjectAttribute = { readonly text14: string | null; readonly text1: string | null; readonly positionState: string | null; + readonly text10: string | null; readonly remarks: string | null; readonly text8: string | null; readonly text11: string | null; @@ -1584,7 +1588,6 @@ export type CollectionObjectAttribute = { readonly text17: string | null; readonly text18: string | null; readonly text19: string | null; - readonly text2: string | null; readonly text20: string | null; readonly text21: string | null; readonly text22: string | null; @@ -1595,6 +1598,7 @@ export type CollectionObjectAttribute = { readonly text27: string | null; readonly text28: string | null; readonly text29: string | null; + readonly text3: string | null; readonly text30: string | null; readonly text31: string | null; readonly text32: string | null; @@ -1605,16 +1609,18 @@ export type CollectionObjectAttribute = { readonly text37: string | null; readonly text38: string | null; readonly text39: string | null; + readonly text4: string | null; readonly text40: string | null; readonly text5: string | null; readonly text6: string | null; readonly text7: string | null; - readonly text8: string | null; readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; + readonly text12: string | null; readonly topDistance: number | null; readonly version: number | null; + readonly text2: string | null; readonly yesNo1: boolean | null; readonly yesNo10: boolean | null; readonly yesNo11: boolean | null; @@ -1652,10 +1658,10 @@ export type CollectionObjectCitation = { readonly fields: { readonly collectionMemberId: number; readonly figureNumber: string | null; - readonly remarks: string | null; readonly isFigured: boolean | null; readonly pageNumber: string | null; readonly plateNumber: string | null; + readonly remarks: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly version: number | null; @@ -2188,7 +2194,9 @@ export type DNASequence = { readonly compT: number | null; readonly extractionDate: string | null; readonly extractionDatePrecision: number | null; + readonly text2: string | null; readonly genbankAccessionNumber: string | null; + readonly text1: string | null; readonly geneSequence: string | null; readonly moleculeType: string | null; readonly number1: number | null; @@ -2198,8 +2206,6 @@ export type DNASequence = { readonly sequenceDate: string | null; readonly sequenceDatePrecision: number | null; readonly targetMarker: string | null; - readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; @@ -2292,8 +2298,8 @@ export type DNASequencingRun = { readonly runByAgent: Agent | null; }; readonly toManyDependent: { - readonly attachments: RA; readonly citations: RA; + readonly attachments: RA; }; readonly toManyIndependent: RR; }; @@ -2455,11 +2461,13 @@ export type Determination = { readonly addendum: string | null; readonly alternateName: string | null; readonly collectionMemberId: number; + readonly confidence: string | null; readonly isCurrent: boolean; readonly determinedDate: string | null; readonly determinedDatePrecision: number | null; readonly featureOrBasis: string | null; readonly guid: string | null; + readonly yesNo1: boolean | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -2473,7 +2481,6 @@ export type Determination = { readonly number4: number | null; readonly number5: number | null; readonly qualifier: string | null; - readonly confidence: string | null; readonly remarks: string | null; readonly subSpQualifier: string | null; readonly text1: string | null; @@ -2487,10 +2494,8 @@ export type Determination = { readonly timestampCreated: string; readonly timestampModified: string | null; readonly typeStatusName: string | null; - readonly text1: string | null; readonly varQualifier: string | null; readonly version: number | null; - readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; readonly yesNo4: boolean | null; @@ -2517,9 +2522,9 @@ export type DeterminationCitation = { readonly collectionMemberId: number; readonly figureNumber: string | null; readonly isFigured: boolean | null; - readonly remarks: string | null; readonly pageNumber: string | null; readonly plateNumber: string | null; + readonly remarks: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly version: number | null; @@ -3087,13 +3092,12 @@ export type FundingAgent = { export type GeoCoordDetail = { readonly tableName: 'GeoCoordDetail'; readonly fields: { - readonly validation: string | null; - readonly source: string | null; readonly errorPolygon: string | null; readonly geoRefAccuracy: number | null; readonly geoRefAccuracyUnits: string | null; readonly geoRefCompiledDate: string | null; readonly geoRefDetDate: string | null; + readonly geoRefDetRef: string | null; readonly geoRefRemarks: string | null; readonly geoRefVerificationStatus: string | null; readonly integer1: number | null; @@ -3101,6 +3105,8 @@ export type GeoCoordDetail = { readonly integer3: number | null; readonly integer4: number | null; readonly integer5: number | null; + readonly maxUncertaintyEst: number | null; + readonly maxUncertaintyEstUnit: string | null; readonly namedPlaceExtent: number | null; readonly noGeoRefBecause: string | null; readonly number1: number | null; @@ -3110,7 +3116,7 @@ export type GeoCoordDetail = { readonly number5: number | null; readonly originalCoordSystem: string | null; readonly protocol: string | null; - readonly geoRefDetRef: string | null; + readonly source: string | null; readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; @@ -3341,8 +3347,8 @@ export type Gift = { readonly srcTaxonomy: string | null; readonly specialConditions: string | null; readonly status: string | null; - readonly text1: string | null; readonly text2: string | null; + readonly text1: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3711,6 +3717,7 @@ export type Loan = { readonly dateClosed: string | null; readonly dateReceived: string | null; readonly yesNo1: boolean | null; + readonly text2: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly integer3: number | null; @@ -3732,7 +3739,6 @@ export type Loan = { readonly specialConditions: string | null; readonly status: string | null; readonly text1: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -3860,43 +3866,44 @@ export type Locality = { readonly fields: { readonly datum: string | null; readonly elevationAccuracy: number | null; - readonly elevationMethod: string | null; readonly gml: string | null; readonly guid: string | null; readonly latLongMethod: string | null; readonly lat1text: string | null; readonly lat2text: string | null; + readonly latLongAccuracy: number | null; readonly latLongType: string | null; readonly latitude1: number | null; readonly latitude2: number | null; - readonly latLongAccuracy: number | null; readonly localityName: string; readonly long1text: string | null; readonly long2text: string | null; readonly longitude1: number | null; readonly longitude2: number | null; - readonly text1: string | null; readonly maxElevation: number | null; readonly minElevation: number | null; - readonly timestampModified: string | null; readonly namedPlace: string | null; + readonly originalElevationUnit: string | null; readonly originalLatLongUnit: number | null; readonly relationToNamedPlace: string | null; readonly remarks: string | null; readonly sgrStatus: number | null; readonly shortName: string | null; readonly srcLatLongUnit: number; + readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; + readonly timestampCreated: string; + readonly timestampModified: string | null; readonly uniqueIdentifier: string | null; - readonly originalElevationUnit: string | null; readonly verbatimElevation: string | null; readonly verbatimLatitude: string | null; readonly verbatimLongitude: string | null; readonly version: number | null; readonly visibility: number | null; + readonly elevationMethod: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; @@ -3910,8 +3917,8 @@ export type Locality = { readonly toOneIndependent: { readonly createdByAgent: Agent | null; readonly discipline: Discipline; - readonly modifiedByAgent: Agent | null; readonly geography: Geography | null; + readonly modifiedByAgent: Agent | null; readonly paleoContext: PaleoContext | null; readonly visibilitySetBy: SpecifyUser | null; }; @@ -3969,6 +3976,7 @@ export type LocalityCitation = { export type LocalityDetail = { readonly tableName: 'LocalityDetail'; readonly fields: { + readonly baseMeridian: string | null; readonly drainage: string | null; readonly endDepth: number | null; readonly endDepthUnit: string | null; @@ -3977,7 +3985,6 @@ export type LocalityDetail = { readonly hucCode: string | null; readonly island: string | null; readonly islandGroup: string | null; - readonly text1: string | null; readonly mgrsZone: string | null; readonly nationalParkName: string | null; readonly number1: number | null; @@ -3994,7 +4001,7 @@ export type LocalityDetail = { readonly startDepth: number | null; readonly startDepthUnit: string | null; readonly startDepthVerbatim: string | null; - readonly baseMeridian: string | null; + readonly text1: string | null; readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; @@ -4378,7 +4385,6 @@ export type Preparation = { readonly date4Precision: number | null; readonly description: string | null; readonly guid: string | null; - readonly text1: string | null; readonly integer1: number | null; readonly integer2: number | null; readonly isOnLoan: boolean | null; @@ -4392,12 +4398,10 @@ export type Preparation = { readonly sampleNumber: string | null; readonly status: string | null; readonly storageLocation: string | null; - readonly text1: string | null; readonly text10: string | null; readonly text11: string | null; readonly text12: string | null; readonly text13: string | null; - readonly text2: string | null; readonly text3: string | null; readonly text4: string | null; readonly text5: string | null; @@ -4407,8 +4411,10 @@ export type Preparation = { readonly text9: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; - readonly version: number | null; + readonly text1: string | null; readonly yesNo1: boolean | null; + readonly version: number | null; + readonly text2: string | null; readonly yesNo2: boolean | null; readonly yesNo3: boolean | null; }; @@ -4785,7 +4791,10 @@ export type RecordSetItem = { export type ReferenceWork = { readonly tableName: 'ReferenceWork'; readonly fields: { + readonly text1: string | null; + readonly workDate: string | null; readonly doi: string | null; + readonly text2: string | null; readonly guid: string | null; readonly isPublished: boolean | null; readonly isbn: string | null; @@ -4796,8 +4805,6 @@ export type ReferenceWork = { readonly placeOfPublication: string | null; readonly publisher: string | null; readonly remarks: string | null; - readonly text1: string | null; - readonly text2: string | null; readonly timestampCreated: string; readonly timestampModified: string | null; readonly title: string; @@ -4806,7 +4813,6 @@ export type ReferenceWork = { readonly url: string | null; readonly version: number | null; readonly volume: string | null; - readonly workDate: string | null; readonly yesNo1: boolean | null; readonly yesNo2: boolean | null; }; @@ -4904,9 +4910,9 @@ export type RepositoryAgreementAttachment = { export type Shipment = { readonly tableName: 'Shipment'; readonly fields: { + readonly numberOfPackages: number | null; readonly insuredForAmount: string | null; readonly shipmentMethod: string | null; - readonly numberOfPackages: number | null; readonly number1: number | null; readonly number2: number | null; readonly remarks: string | null; @@ -5620,7 +5626,6 @@ export type Taxon = { readonly colStatus: string | null; readonly commonName: string | null; readonly cultivarName: string | null; - readonly environmentalProtectionStatus: string | null; readonly esaStatus: string | null; readonly fullName: string | null; readonly groupNumber: string | null; @@ -5644,6 +5649,7 @@ export type Taxon = { readonly number3: number | null; readonly number4: number | null; readonly number5: number | null; + readonly environmentalProtectionStatus: string | null; readonly rankId: number; readonly remarks: string | null; readonly source: string | null; @@ -6514,8 +6520,7 @@ export type CollectionObjectGroup = { readonly parentCojo: CollectionObjectGroupJoin | null; }; readonly toManyDependent: { - readonly cojo: RA; - readonly parentcojos: RA; + readonly children: RA; }; readonly toManyIndependent: RR; }; From c52e810a63a7d614b90b28aba8d292138bb6381d Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 30 Oct 2024 14:13:24 -0400 Subject: [PATCH 126/132] Update business rule with new types --- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 04ef2709937..7a596b2392e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -211,7 +211,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { // The first COJO CO will automatically have isPrimary set to True when the COG type is 'consolidated' cog.rgetPromise('cogType').then((cogtype) => { if (cogtype.get('type') === cogTypes.CONSOLIDATED) { - const cojos = cog.getDependentResource('cojo'); + const cojos = cog.getDependentResource('children'); // Set first CO in COG to primary cojos?.models .find((cojo) => cojo.get('childCo') !== null) From aac3197eec6b6fe1d32c99141bc866187c9ddfc8 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 30 Oct 2024 14:23:11 -0400 Subject: [PATCH 127/132] Fix typecheck --- .../lib/components/DataModel/__tests__/businessRules.test.ts | 2 +- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 1 - .../js_src/lib/components/DataModel/businessRuleUtils.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 34c0a1d092c..61c84052045 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -214,7 +214,7 @@ describe('CollectionObjectGroup business rules', () => { parentCog: getResourceApiUrl('CollectionObjectGroup', 1), }); - cog.set('cojo', [cojo1, cojo2]); + cog.set('children', [cojo1, cojo2]); return { cog, cojo1, cojo2 }; }; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 7a596b2392e..bcf469717ff 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -3,7 +3,6 @@ import { f } from '../../utils/functools'; import type { BusinessRuleResult } from './businessRules'; import { CURRENT_DETERMINATION_KEY, - DETERMINATION_TAXON_KEY, ensureSingleCollectionObjectCheck, hasNoCurrentDetermination, } from './businessRuleUtils'; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts index f0a811a1e7e..c72ae4481fa 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts @@ -4,7 +4,6 @@ import type { CollectionObjectGroupJoin, Determination } from './types'; // Save blocker keys used in businessRuleDefs.ts export const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; -export const DETERMINATION_TAXON_KEY = 'determination-taxon'; /** * From 22de47c5087631a9f2cc2044426e2574aa17c241 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 30 Oct 2024 14:47:39 -0400 Subject: [PATCH 128/132] fix snapshots --- .../__snapshots__/utils.test.ts.snap | 4 + .../__snapshots__/specifyTable.test.ts.snap | 15 + .../__snapshots__/createView.test.ts.snap | 9 + .../__snapshots__/formatters.test.ts.snap | 37 + .../Forms/__tests__/parentTables.test.tsx | 5 + .../WbPlanView/__tests__/automapper.test.ts | 2 + .../tests/ajax/static/context/datamodel.json | 1397 ++++++++++++++++- 7 files changed, 1462 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap index bbe99dcb41c..eaacbe6f3ae 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap @@ -29,6 +29,8 @@ exports[`allTablesWithAttachments 1`] = ` "[table Storage]", "[table Taxon]", "[table TreatmentEvent]", + "[table AbsoluteAge]", + "[table RelativeAge]", ] `; @@ -62,5 +64,7 @@ exports[`attachmentRelatedTables 1`] = ` "StorageAttachment", "TaxonAttachment", "TreatmentEventAttachment", + "AbsoluteAgeAttachment", + "RelativeAgeAttachment", ] `; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap index 58a082a2950..74d383dda9e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap @@ -66,6 +66,7 @@ exports[`fields are loaded 1`] = ` "[literalField CollectionObject.yesNo4]", "[literalField CollectionObject.yesNo5]", "[literalField CollectionObject.yesNo6]", + "[relationship CollectionObject.absoluteAges]", "[relationship CollectionObject.accession]", "[relationship CollectionObject.agent1]", "[relationship CollectionObject.appraisal]", @@ -96,6 +97,7 @@ exports[`fields are loaded 1`] = ` "[relationship CollectionObject.paleoContext]", "[relationship CollectionObject.preparations]", "[relationship CollectionObject.projects]", + "[relationship CollectionObject.relativeAges]", "[relationship CollectionObject.rightSideRels]", "[relationship CollectionObject.treatmentEvents]", "[relationship CollectionObject.visibilitySetBy]", @@ -105,6 +107,7 @@ exports[`fields are loaded 1`] = ` exports[`indexed fields are loaded 1`] = ` { + "absoluteAges": "[relationship CollectionObject.absoluteAges]", "accession": "[relationship CollectionObject.accession]", "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", "age": "[literalField CollectionObject.age]", @@ -170,6 +173,7 @@ exports[`indexed fields are loaded 1`] = ` "preparations": "[relationship CollectionObject.preparations]", "projectNumber": "[literalField CollectionObject.projectNumber]", "projects": "[relationship CollectionObject.projects]", + "relativeAges": "[relationship CollectionObject.relativeAges]", "remarks": "[literalField CollectionObject.remarks]", "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", @@ -1319,6 +1323,7 @@ exports[`localization is loaded 1`] = ` exports[`relationships are loaded 1`] = ` [ + "[relationship CollectionObject.absoluteAges]", "[relationship CollectionObject.accession]", "[relationship CollectionObject.agent1]", "[relationship CollectionObject.appraisal]", @@ -1349,6 +1354,7 @@ exports[`relationships are loaded 1`] = ` "[relationship CollectionObject.paleoContext]", "[relationship CollectionObject.preparations]", "[relationship CollectionObject.projects]", + "[relationship CollectionObject.relativeAges]", "[relationship CollectionObject.rightSideRels]", "[relationship CollectionObject.treatmentEvents]", "[relationship CollectionObject.visibilitySetBy]", @@ -1358,6 +1364,9 @@ exports[`relationships are loaded 1`] = ` exports[`tableScoping 1`] = ` { + "AbsoluteAge": "collectionObject", + "AbsoluteAgeAttachment": "absoluteAge > collectionObject", + "AbsoluteAgeCitation": "absoluteAge > collectionObject", "Accession": "division", "AccessionAgent": "accession > division", "AccessionAttachment": "accession > division", @@ -1505,6 +1514,9 @@ exports[`tableScoping 1`] = ` "RecordSetItem": undefined, "ReferenceWork": undefined, "ReferenceWorkAttachment": undefined, + "RelativeAge": "collectionObject", + "RelativeAgeAttachment": "relativeAge > collectionObject", + "RelativeAgeCitation": "relativeAge > collectionObject", "RepositoryAgreement": "division", "RepositoryAgreementAttachment": "repositoryAgreement > division", "Role": "collection", @@ -1548,6 +1560,9 @@ exports[`tableScoping 1`] = ` "TaxonCitation": "taxon > definition > discipline", "TaxonTreeDef": "discipline", "TaxonTreeDefItem": "treeDef > discipline", + "TectonicUnit": "definition > discipline", + "TectonicUnitTreeDef": "discipline", + "TectonicUnitTreeDefItem": "treeDef > discipline", "TreatmentEvent": "collectionObject", "TreatmentEventAttachment": "treatmentEvent > collectionObject", "UniquenessRule": "discipline", diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap index 8d11c38c162..3b32328170c 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap @@ -145,5 +145,14 @@ exports[`Tables with form tables computed correctly 1`] = ` "[table CollectionObjectGroup]", "[table CollectionObjectGroupJoin]", "[table CollectionObjectGroupType]", + "[table AbsoluteAge]", + "[table RelativeAge]", + "[table AbsoluteAgeAttachment]", + "[table RelativeAgeAttachment]", + "[table AbsoluteAgeCitation]", + "[table RelativeAgeCitation]", + "[table TectonicUnitTreeDef]", + "[table TectonicUnitTreeDefItem]", + "[table TectonicUnit]", ] `; diff --git a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap index 2cd4eb46a74..3c15ef324f7 100644 --- a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap @@ -2032,6 +2032,16 @@ exports[`Formatters are fetched and parsed correctly 1`] = ` exports[`getMainTableFields 1`] = ` { + "AbsoluteAge": [ + "ageType", + "datingMethod", + ], + "AbsoluteAgeAttachment": [], + "AbsoluteAgeCitation": [ + "figureNumber", + "pageNumber", + "plateNumber", + ], "Accession": [ "accessionNumber", "status", @@ -2604,6 +2614,16 @@ exports[`getMainTableFields 1`] = ` "volume", ], "ReferenceWorkAttachment": [], + "RelativeAge": [ + "ageType", + "datingMethod", + ], + "RelativeAgeAttachment": [], + "RelativeAgeCitation": [ + "figureNumber", + "pageNumber", + "plateNumber", + ], "RepositoryAgreement": [ "repositoryAgreementNumber", "status", @@ -2743,6 +2763,23 @@ exports[`getMainTableFields 1`] = ` "textBefore", "title", ], + "TectonicUnit": [ + "name", + "fullName", + "guid", + "text1", + "text2", + ], + "TectonicUnitTreeDef": [ + "name", + ], + "TectonicUnitTreeDefItem": [ + "name", + "fullNameSeparator", + "textAfter", + "textBefore", + "title", + ], "TreatmentEvent": [ "treatmentNumber", "type", diff --git a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx index 66f10effad8..4ffbd9130bc 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx @@ -6,6 +6,8 @@ requireContext(); test('Parent table relationships are calculated properly', () => expect(parentTableRelationship()).toMatchInlineSnapshot(` { + "AbsoluteAgeAttachment": "[relationship AbsoluteAgeAttachment.absoluteAge]", + "AbsoluteAgeCitation": "[relationship AbsoluteAgeCitation.absoluteAge]", "AccessionAgent": "[relationship AccessionAgent.accession]", "AccessionAttachment": "[relationship AccessionAttachment.accession]", "AccessionAuthorization": "[relationship AccessionAuthorization.accession]", @@ -86,6 +88,8 @@ test('Parent table relationships are calculated properly', () => "PreparationProperty": "[relationship PreparationProperty.preparation]", "RecordSetItem": "[relationship RecordSetItem.recordSet]", "ReferenceWorkAttachment": "[relationship ReferenceWorkAttachment.referenceWork]", + "RelativeAgeAttachment": "[relationship RelativeAgeAttachment.relativeAge]", + "RelativeAgeCitation": "[relationship RelativeAgeCitation.relativeAge]", "RepositoryAgreementAttachment": "[relationship RepositoryAgreementAttachment.repositoryAgreement]", "RolePolicy": "[relationship RolePolicy.role]", "SpAppResourceData": "[relationship SpAppResourceData.spAppResource]", @@ -99,6 +103,7 @@ test('Parent table relationships are calculated properly', () => "TaxonAttachment": "[relationship TaxonAttachment.taxon]", "TaxonCitation": "[relationship TaxonCitation.taxon]", "TaxonTreeDefItem": "[relationship TaxonTreeDefItem.treeDef]", + "TectonicUnitTreeDefItem": "[relationship TectonicUnitTreeDefItem.treeDef]", "TreatmentEventAttachment": "[relationship TreatmentEventAttachment.treatmentEvent]", "UniquenessRuleField": "[relationship UniquenessRuleField.uniquenessrule]", "WorkbenchRow": "[relationship WorkbenchRow.workbench]", diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts index 7aa22c6671a..d66ab3e8fb8 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts @@ -216,5 +216,7 @@ test('circular tables are calculated correctly', () => "[table StorageTreeDefItem]", "[table Taxon]", "[table TaxonTreeDefItem]", + "[table TectonicUnitTreeDefItem]", + "[table TectonicUnit]", ] `)); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json index dd0438cbf64..a89de5ec329 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json @@ -7568,6 +7568,22 @@ "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", "otherSideName": "childco" + }, + { + "name": "absoluteAges", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "AbsoluteAge", + "otherSideName": "collectionObject" + }, + { + "name": "relativeAges", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "RelativeAge", + "otherSideName": "collectionObject" } ], "fieldAliases": [] @@ -14835,6 +14851,15 @@ "column": "LithoStratTreeDefID", "otherSideName": "disciplines" }, + { + "name": "tectonicUnitTreeDef", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDef", + "column": "TectonicUnitTreeDefID", + "otherSideName": "disciplines" + }, { "name": "modifiedByAgent", "type": "many-to-one", @@ -18265,7 +18290,7 @@ { "name": "discipline", "type": "many-to-one", - "required": true, + "required": false, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -18797,7 +18822,7 @@ { "name": "discipline", "type": "many-to-one", - "required": true, + "required": false, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -24424,6 +24449,15 @@ "column": "LithoStratID", "otherSideName": "paleoContexts" }, + { + "name": "tectonicUnit", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnit", + "column": "TectonicUnitID", + "otherSideName": "paleoContexts" + }, { "name": "localities", "type": "one-to-many", @@ -38394,12 +38428,13 @@ "column": "COGTypeID" }, { - "name": "parentCojos", - "type": "one-to-many", + "name": "parentCojo", + "type": "many-to-one", "required": false, - "dependent": true, + "dependent": false, "relatedModelName": "CollectionObjectGroupJoin", - "otherSideName": "parentCog" + "column": "CollectionObjectGroupJoinID", + "otherSideName": "collectionobjectgroup" }, { "name": "cojo", @@ -38409,6 +38444,14 @@ "relatedModelName": "CollectionObjectGroupJoin", "otherSideName": "childCog" }, + { + "name": "children", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "CollectionObjectGroupJoin", + "otherSideName": "parentCog" + }, { "name": "createdByAgent", "type": "many-to-one", @@ -38568,7 +38611,7 @@ "dependent": false, "relatedModelName": "CollectionObjectGroup", "column": "ParentCOGID", - "otherSideName": "parentcojos" + "otherSideName": "children" }, { "name": "childCog", @@ -38587,6 +38630,14 @@ "relatedModelName": "CollectionObject", "column": "ChildCOID", "otherSideName": "cojo" + }, + { + "name": "collectionobjectgroup", + "type": "one-to-many", + "required": false, + "dependent": false, + "relatedModelName": "CollectionObjectGroup", + "otherSideName": "parentCojo" } ], "fieldAliases": [] @@ -38669,5 +38720,1337 @@ } ], "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.AbsoluteAge", + "table": "absoluteage", + "tableId": 1019, + "system": false, + "idColumn": "AbsoluteAgeID", + "idFieldName": "absoluteAgeId", + "fields": [ + { + "name": "absoluteAge", + "column": "AbsoluteAge", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "ageType", + "column": "AgeType", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 64 + }, + { + "name": "ageUncertainty", + "column": "AgeUncertainty", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "collectionDate", + "column": "CollectionDate", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "date1", + "column": "Date1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "date2", + "column": "Date2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "datingMethod", + "column": "DatingMethod", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 64 + }, + { + "name": "datingMethodRemarks", + "column": "DatingMethodRemarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "number1", + "column": "Number1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "number2", + "column": "Number2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "text1", + "column": "Text1", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "text2", + "column": "Text2", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "yesno1", + "column": "YesNo1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "yesno2", + "column": "YesNo2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + } + ], + "relationships": [ + { + "name": "agent1", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "Agent1ID" + }, + { + "name": "absoluteAgeAttachments", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "AbsoluteAgeAttachment", + "otherSideName": "absoluteAge" + }, + { + "name": "collectionObject", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "CollectionObject", + "column": "CollectionObjectID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.RelativeAge", + "table": "relativeage", + "tableId": 1020, + "system": false, + "idColumn": "RelativeAgeID", + "idFieldName": "relativeAgeId", + "fields": [ + { + "name": "ageType", + "column": "AgeType", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 64 + }, + { + "name": "ageUncertainty", + "column": "AgeUncertainty", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "collectionDate", + "column": "CollectionDate", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "date1", + "column": "Date1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "date2", + "column": "Date2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.util.Date" + }, + { + "name": "datingMethod", + "column": "DatingMethod", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 64 + }, + { + "name": "datingMethodRemarks", + "column": "DatingMethodRemarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "number1", + "column": "Number1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "number2", + "column": "Number2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "relativeAgePeriod", + "column": "RelativeAgePeriod", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "text1", + "column": "Text1", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "text2", + "column": "Text2", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "verbatimName", + "column": "VerbatimName", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "verbatimPeriod", + "column": "VerbatimPeriod", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "yesno1", + "column": "YesNo1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "yesno2", + "column": "YesNo2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + } + ], + "relationships": [ + { + "name": "ageName", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "GeologicTimePeriod", + "column": "AgeNameID" + }, + { + "name": "ageNameEnd", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "GeologicTimePeriod", + "column": "AgeNameEndID" + }, + { + "name": "agent1", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "Agent1ID" + }, + { + "name": "agent2", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "Agent2ID" + }, + { + "name": "relativeAgeAttachments", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "RelativeAgeAttachment", + "otherSideName": "relativeAge" + }, + { + "name": "collectionObject", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "CollectionObject", + "column": "CollectionObjectID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.AbsoluteAgeAttachment", + "table": "absoluteageattachment", + "tableId": 1021, + "system": false, + "idColumn": "AbsoluteAgeAttachmentID", + "idFieldName": "absoluteAgeAttachmentId", + "fields": [ + { + "name": "ordinal", + "column": "Ordinal", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "attachment", + "type": "many-to-one", + "required": true, + "dependent": true, + "relatedModelName": "Attachment", + "column": "AttachmentID", + "otherSideName": "absoluteAgeAttachments" + }, + { + "name": "collectionMember", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "Collection", + "column": "CollectionMemberID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "absoluteAge", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "AbsoluteAge", + "column": "AbsoluteAgeID", + "otherSideName": "absoluteAgeAttachments" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.RelativeAgeAttachment", + "table": "relativeageattachment", + "tableId": 1022, + "system": false, + "idColumn": "RelativeAgeAttachmentID", + "idFieldName": "relativeAgeAttachmentId", + "fields": [ + { + "name": "ordinal", + "column": "Ordinal", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "attachment", + "type": "many-to-one", + "required": true, + "dependent": true, + "relatedModelName": "Attachment", + "column": "AttachmentID", + "otherSideName": "relativeAgeAttachments" + }, + { + "name": "collectionMember", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "Collection", + "column": "CollectionMemberID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "relativeAge", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "RelativeAge", + "column": "RelativeAgeID", + "otherSideName": "relativeAgeAttachments" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.AbsoluteAgeCitation", + "table": "absoluteagecitation", + "tableId": 1023, + "system": false, + "idColumn": "AbsoluteAgeCitationID", + "idFieldName": "absoluteAgeCitationId", + "fields": [ + { + "name": "figureNumber", + "column": "FigureNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "isFigured", + "column": "IsFigured", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "pageNumber", + "column": "PageNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "plateNumber", + "column": "PlateNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "absoluteAge", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "AbsoluteAge", + "column": "AbsoluteAgeID" + }, + { + "name": "collectionMember", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "Collection", + "column": "CollectionMemberID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "referenceWork", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "ReferenceWork", + "column": "ReferenceWorkID" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.RelativeAgeCitation", + "table": "relativeagecitation", + "tableId": 1024, + "system": false, + "idColumn": "RelativeAgeCitationID", + "idFieldName": "relativeAgeCitationId", + "fields": [ + { + "name": "figureNumber", + "column": "FigureNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "isFigured", + "column": "IsFigured", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "pageNumber", + "column": "PageNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "plateNumber", + "column": "PlateNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 50 + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "collectionMember", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "Collection", + "column": "CollectionMemberID" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "referenceWork", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "ReferenceWork", + "column": "ReferenceWorkID" + }, + { + "name": "relativeAge", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "RelativeAge", + "column": "RelativeAgeID" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.TectonicUnitTreeDef", + "table": "tectonicunittreedef", + "tableId": 1025, + "system": false, + "idColumn": "TectonicUnitTreeDefID", + "idFieldName": "tectonicUnitTreeDefId", + "fields": [ + { + "name": "fullNameDirection", + "column": "FullNameDirection", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "name", + "column": "Name", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "discipline", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "Discipline", + "column": "DisciplineID", + "otherSideName": "tectonicUnitTreeDefs" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "treeDefItems", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "TectonicUnitTreeDefItem", + "otherSideName": "treeDef" + }, + { + "name": "treeEntries", + "type": "one-to-many", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnit", + "otherSideName": "definition" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.TectonicUnitTreeDefItem", + "table": "tectonicunittreedefitem", + "tableId": 1026, + "system": false, + "idColumn": "TectonicUnitTreeDefItemID", + "idFieldName": "tectonicUnitTreeDefItemId", + "fields": [ + { + "name": "fullNameSeparator", + "column": "FullNameSeparator", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "isEnforced", + "column": "IsEnforced", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "isInFullName", + "column": "IsInFullName", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "name", + "column": "Name", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "rankId", + "column": "RankID", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.Integer" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "textAfter", + "column": "TextAfter", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "textBefore", + "column": "TextBefore", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "title", + "column": "Title", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + } + ], + "relationships": [ + { + "name": "children", + "type": "one-to-many", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDefItem", + "otherSideName": "parent" + }, + { + "name": "createdbyagent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "parent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDefItem", + "column": "ParentItemID", + "otherSideName": "children" + }, + { + "name": "treeDef", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDef", + "column": "TectonicUnitTreeDefID", + "otherSideName": "treeDefItems" + }, + { + "name": "treeEntries", + "type": "one-to-many", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnit", + "otherSideName": "definitionItem" + } + ], + "fieldAliases": [] + }, + { + "classname": "edu.ku.brc.specify.datamodel.TectonicUnit", + "table": "tectonicunit", + "tableId": 1027, + "system": false, + "idColumn": "TectonicUnitID", + "idFieldName": "tectonicUnitId", + "fields": [ + { + "name": "fullName", + "column": "FullName", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "guid", + "column": "GUID", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 128 + }, + { + "name": "highestChildNodeNumber", + "column": "HighestChildNodeNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "isAccepted", + "column": "IsAccepted", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.Boolean" + }, + { + "name": "name", + "column": "Name", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "nodeNumber", + "column": "NodeNumber", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "number1", + "column": "Number1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "number2", + "column": "Number2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.math.BigDecimal" + }, + { + "name": "rankId", + "column": "RankID", + "indexed": false, + "unique": false, + "required": true, + "type": "java.lang.Integer" + }, + { + "name": "remarks", + "column": "Remarks", + "indexed": false, + "unique": false, + "required": false, + "type": "text" + }, + { + "name": "text1", + "column": "Text1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "text2", + "column": "Text2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.String", + "length": 255 + }, + { + "name": "timestampCreated", + "column": "TimestampCreated", + "indexed": false, + "unique": false, + "required": true, + "type": "java.sql.Timestamp" + }, + { + "name": "timestampModified", + "column": "TimestampModified", + "indexed": false, + "unique": false, + "required": false, + "type": "java.sql.Timestamp" + }, + { + "name": "version", + "column": "Version", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Integer" + }, + { + "name": "yesno1", + "column": "YesNo1", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + }, + { + "name": "yesno2", + "column": "YesNo2", + "indexed": false, + "unique": false, + "required": false, + "type": "java.lang.Boolean" + } + ], + "relationships": [ + { + "name": "acceptedChildren", + "type": "one-to-many", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnit", + "otherSideName": "acceptedTectonicUnit" + }, + { + "name": "acceptedTectonicUnit", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "TectonicUnit", + "column": "AcceptedID", + "otherSideName": "acceptedChildren" + }, + { + "name": "createdByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "CreatedByAgentID" + }, + { + "name": "modifiedByAgent", + "type": "many-to-one", + "required": false, + "dependent": false, + "relatedModelName": "Agent", + "column": "ModifiedByAgentID" + }, + { + "name": "parent", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "TectonicUnit", + "column": "ParentID" + }, + { + "name": "definition", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDef", + "column": "TectonicUnitTreeDefID", + "otherSideName": "treeEntries" + }, + { + "name": "definitionItem", + "type": "many-to-one", + "required": true, + "dependent": false, + "relatedModelName": "TectonicUnitTreeDefItem", + "column": "TectonicUnitTreeDefItemID", + "otherSideName": "treeEntries" + } + ], + "fieldAliases": [] } ] \ No newline at end of file From eb0c85650d5fb1dece8762570d2fc44113c0faa2 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 1 Nov 2024 10:14:19 -0400 Subject: [PATCH 129/132] Revert "fix snapshots" This reverts commit 22de47c5087631a9f2cc2044426e2574aa17c241. --- .../__snapshots__/utils.test.ts.snap | 4 - .../__snapshots__/specifyTable.test.ts.snap | 15 - .../__snapshots__/createView.test.ts.snap | 9 - .../__snapshots__/formatters.test.ts.snap | 37 - .../Forms/__tests__/parentTables.test.tsx | 5 - .../WbPlanView/__tests__/automapper.test.ts | 2 - .../tests/ajax/static/context/datamodel.json | 1397 +---------------- 7 files changed, 7 insertions(+), 1462 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap index eaacbe6f3ae..bbe99dcb41c 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/__snapshots__/utils.test.ts.snap @@ -29,8 +29,6 @@ exports[`allTablesWithAttachments 1`] = ` "[table Storage]", "[table Taxon]", "[table TreatmentEvent]", - "[table AbsoluteAge]", - "[table RelativeAge]", ] `; @@ -64,7 +62,5 @@ exports[`attachmentRelatedTables 1`] = ` "StorageAttachment", "TaxonAttachment", "TreatmentEventAttachment", - "AbsoluteAgeAttachment", - "RelativeAgeAttachment", ] `; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap index 74d383dda9e..58a082a2950 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap @@ -66,7 +66,6 @@ exports[`fields are loaded 1`] = ` "[literalField CollectionObject.yesNo4]", "[literalField CollectionObject.yesNo5]", "[literalField CollectionObject.yesNo6]", - "[relationship CollectionObject.absoluteAges]", "[relationship CollectionObject.accession]", "[relationship CollectionObject.agent1]", "[relationship CollectionObject.appraisal]", @@ -97,7 +96,6 @@ exports[`fields are loaded 1`] = ` "[relationship CollectionObject.paleoContext]", "[relationship CollectionObject.preparations]", "[relationship CollectionObject.projects]", - "[relationship CollectionObject.relativeAges]", "[relationship CollectionObject.rightSideRels]", "[relationship CollectionObject.treatmentEvents]", "[relationship CollectionObject.visibilitySetBy]", @@ -107,7 +105,6 @@ exports[`fields are loaded 1`] = ` exports[`indexed fields are loaded 1`] = ` { - "absoluteAges": "[relationship CollectionObject.absoluteAges]", "accession": "[relationship CollectionObject.accession]", "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", "age": "[literalField CollectionObject.age]", @@ -173,7 +170,6 @@ exports[`indexed fields are loaded 1`] = ` "preparations": "[relationship CollectionObject.preparations]", "projectNumber": "[literalField CollectionObject.projectNumber]", "projects": "[relationship CollectionObject.projects]", - "relativeAges": "[relationship CollectionObject.relativeAges]", "remarks": "[literalField CollectionObject.remarks]", "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", @@ -1323,7 +1319,6 @@ exports[`localization is loaded 1`] = ` exports[`relationships are loaded 1`] = ` [ - "[relationship CollectionObject.absoluteAges]", "[relationship CollectionObject.accession]", "[relationship CollectionObject.agent1]", "[relationship CollectionObject.appraisal]", @@ -1354,7 +1349,6 @@ exports[`relationships are loaded 1`] = ` "[relationship CollectionObject.paleoContext]", "[relationship CollectionObject.preparations]", "[relationship CollectionObject.projects]", - "[relationship CollectionObject.relativeAges]", "[relationship CollectionObject.rightSideRels]", "[relationship CollectionObject.treatmentEvents]", "[relationship CollectionObject.visibilitySetBy]", @@ -1364,9 +1358,6 @@ exports[`relationships are loaded 1`] = ` exports[`tableScoping 1`] = ` { - "AbsoluteAge": "collectionObject", - "AbsoluteAgeAttachment": "absoluteAge > collectionObject", - "AbsoluteAgeCitation": "absoluteAge > collectionObject", "Accession": "division", "AccessionAgent": "accession > division", "AccessionAttachment": "accession > division", @@ -1514,9 +1505,6 @@ exports[`tableScoping 1`] = ` "RecordSetItem": undefined, "ReferenceWork": undefined, "ReferenceWorkAttachment": undefined, - "RelativeAge": "collectionObject", - "RelativeAgeAttachment": "relativeAge > collectionObject", - "RelativeAgeCitation": "relativeAge > collectionObject", "RepositoryAgreement": "division", "RepositoryAgreementAttachment": "repositoryAgreement > division", "Role": "collection", @@ -1560,9 +1548,6 @@ exports[`tableScoping 1`] = ` "TaxonCitation": "taxon > definition > discipline", "TaxonTreeDef": "discipline", "TaxonTreeDefItem": "treeDef > discipline", - "TectonicUnit": "definition > discipline", - "TectonicUnitTreeDef": "discipline", - "TectonicUnitTreeDefItem": "treeDef > discipline", "TreatmentEvent": "collectionObject", "TreatmentEventAttachment": "treatmentEvent > collectionObject", "UniquenessRule": "discipline", diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap index 3b32328170c..8d11c38c162 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/__tests__/__snapshots__/createView.test.ts.snap @@ -145,14 +145,5 @@ exports[`Tables with form tables computed correctly 1`] = ` "[table CollectionObjectGroup]", "[table CollectionObjectGroupJoin]", "[table CollectionObjectGroupType]", - "[table AbsoluteAge]", - "[table RelativeAge]", - "[table AbsoluteAgeAttachment]", - "[table RelativeAgeAttachment]", - "[table AbsoluteAgeCitation]", - "[table RelativeAgeCitation]", - "[table TectonicUnitTreeDef]", - "[table TectonicUnitTreeDefItem]", - "[table TectonicUnit]", ] `; diff --git a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap index 3c15ef324f7..2cd4eb46a74 100644 --- a/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Formatters/__tests__/__snapshots__/formatters.test.ts.snap @@ -2032,16 +2032,6 @@ exports[`Formatters are fetched and parsed correctly 1`] = ` exports[`getMainTableFields 1`] = ` { - "AbsoluteAge": [ - "ageType", - "datingMethod", - ], - "AbsoluteAgeAttachment": [], - "AbsoluteAgeCitation": [ - "figureNumber", - "pageNumber", - "plateNumber", - ], "Accession": [ "accessionNumber", "status", @@ -2614,16 +2604,6 @@ exports[`getMainTableFields 1`] = ` "volume", ], "ReferenceWorkAttachment": [], - "RelativeAge": [ - "ageType", - "datingMethod", - ], - "RelativeAgeAttachment": [], - "RelativeAgeCitation": [ - "figureNumber", - "pageNumber", - "plateNumber", - ], "RepositoryAgreement": [ "repositoryAgreementNumber", "status", @@ -2763,23 +2743,6 @@ exports[`getMainTableFields 1`] = ` "textBefore", "title", ], - "TectonicUnit": [ - "name", - "fullName", - "guid", - "text1", - "text2", - ], - "TectonicUnitTreeDef": [ - "name", - ], - "TectonicUnitTreeDefItem": [ - "name", - "fullNameSeparator", - "textAfter", - "textBefore", - "title", - ], "TreatmentEvent": [ "treatmentNumber", "type", diff --git a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx index 4ffbd9130bc..66f10effad8 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/__tests__/parentTables.test.tsx @@ -6,8 +6,6 @@ requireContext(); test('Parent table relationships are calculated properly', () => expect(parentTableRelationship()).toMatchInlineSnapshot(` { - "AbsoluteAgeAttachment": "[relationship AbsoluteAgeAttachment.absoluteAge]", - "AbsoluteAgeCitation": "[relationship AbsoluteAgeCitation.absoluteAge]", "AccessionAgent": "[relationship AccessionAgent.accession]", "AccessionAttachment": "[relationship AccessionAttachment.accession]", "AccessionAuthorization": "[relationship AccessionAuthorization.accession]", @@ -88,8 +86,6 @@ test('Parent table relationships are calculated properly', () => "PreparationProperty": "[relationship PreparationProperty.preparation]", "RecordSetItem": "[relationship RecordSetItem.recordSet]", "ReferenceWorkAttachment": "[relationship ReferenceWorkAttachment.referenceWork]", - "RelativeAgeAttachment": "[relationship RelativeAgeAttachment.relativeAge]", - "RelativeAgeCitation": "[relationship RelativeAgeCitation.relativeAge]", "RepositoryAgreementAttachment": "[relationship RepositoryAgreementAttachment.repositoryAgreement]", "RolePolicy": "[relationship RolePolicy.role]", "SpAppResourceData": "[relationship SpAppResourceData.spAppResource]", @@ -103,7 +99,6 @@ test('Parent table relationships are calculated properly', () => "TaxonAttachment": "[relationship TaxonAttachment.taxon]", "TaxonCitation": "[relationship TaxonCitation.taxon]", "TaxonTreeDefItem": "[relationship TaxonTreeDefItem.treeDef]", - "TectonicUnitTreeDefItem": "[relationship TectonicUnitTreeDefItem.treeDef]", "TreatmentEventAttachment": "[relationship TreatmentEventAttachment.treatmentEvent]", "UniquenessRuleField": "[relationship UniquenessRuleField.uniquenessrule]", "WorkbenchRow": "[relationship WorkbenchRow.workbench]", diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts index d66ab3e8fb8..7aa22c6671a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts @@ -216,7 +216,5 @@ test('circular tables are calculated correctly', () => "[table StorageTreeDefItem]", "[table Taxon]", "[table TaxonTreeDefItem]", - "[table TectonicUnitTreeDefItem]", - "[table TectonicUnit]", ] `)); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json index a89de5ec329..dd0438cbf64 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json @@ -7568,22 +7568,6 @@ "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", "otherSideName": "childco" - }, - { - "name": "absoluteAges", - "type": "one-to-many", - "required": false, - "dependent": true, - "relatedModelName": "AbsoluteAge", - "otherSideName": "collectionObject" - }, - { - "name": "relativeAges", - "type": "one-to-many", - "required": false, - "dependent": true, - "relatedModelName": "RelativeAge", - "otherSideName": "collectionObject" } ], "fieldAliases": [] @@ -14851,15 +14835,6 @@ "column": "LithoStratTreeDefID", "otherSideName": "disciplines" }, - { - "name": "tectonicUnitTreeDef", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDef", - "column": "TectonicUnitTreeDefID", - "otherSideName": "disciplines" - }, { "name": "modifiedByAgent", "type": "many-to-one", @@ -18290,7 +18265,7 @@ { "name": "discipline", "type": "many-to-one", - "required": false, + "required": true, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -18822,7 +18797,7 @@ { "name": "discipline", "type": "many-to-one", - "required": false, + "required": true, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -24449,15 +24424,6 @@ "column": "LithoStratID", "otherSideName": "paleoContexts" }, - { - "name": "tectonicUnit", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnit", - "column": "TectonicUnitID", - "otherSideName": "paleoContexts" - }, { "name": "localities", "type": "one-to-many", @@ -38428,29 +38394,20 @@ "column": "COGTypeID" }, { - "name": "parentCojo", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "CollectionObjectGroupJoin", - "column": "CollectionObjectGroupJoinID", - "otherSideName": "collectionobjectgroup" - }, - { - "name": "cojo", + "name": "parentCojos", "type": "one-to-many", "required": false, "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", - "otherSideName": "childCog" + "otherSideName": "parentCog" }, { - "name": "children", + "name": "cojo", "type": "one-to-many", "required": false, "dependent": true, "relatedModelName": "CollectionObjectGroupJoin", - "otherSideName": "parentCog" + "otherSideName": "childCog" }, { "name": "createdByAgent", @@ -38611,7 +38568,7 @@ "dependent": false, "relatedModelName": "CollectionObjectGroup", "column": "ParentCOGID", - "otherSideName": "children" + "otherSideName": "parentcojos" }, { "name": "childCog", @@ -38630,14 +38587,6 @@ "relatedModelName": "CollectionObject", "column": "ChildCOID", "otherSideName": "cojo" - }, - { - "name": "collectionobjectgroup", - "type": "one-to-many", - "required": false, - "dependent": false, - "relatedModelName": "CollectionObjectGroup", - "otherSideName": "parentCojo" } ], "fieldAliases": [] @@ -38720,1337 +38669,5 @@ } ], "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.AbsoluteAge", - "table": "absoluteage", - "tableId": 1019, - "system": false, - "idColumn": "AbsoluteAgeID", - "idFieldName": "absoluteAgeId", - "fields": [ - { - "name": "absoluteAge", - "column": "AbsoluteAge", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "ageType", - "column": "AgeType", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 64 - }, - { - "name": "ageUncertainty", - "column": "AgeUncertainty", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "collectionDate", - "column": "CollectionDate", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "date1", - "column": "Date1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "date2", - "column": "Date2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "datingMethod", - "column": "DatingMethod", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 64 - }, - { - "name": "datingMethodRemarks", - "column": "DatingMethodRemarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "number1", - "column": "Number1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "number2", - "column": "Number2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "text1", - "column": "Text1", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "text2", - "column": "Text2", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "yesno1", - "column": "YesNo1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "yesno2", - "column": "YesNo2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - } - ], - "relationships": [ - { - "name": "agent1", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "Agent1ID" - }, - { - "name": "absoluteAgeAttachments", - "type": "one-to-many", - "required": false, - "dependent": true, - "relatedModelName": "AbsoluteAgeAttachment", - "otherSideName": "absoluteAge" - }, - { - "name": "collectionObject", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "CollectionObject", - "column": "CollectionObjectID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.RelativeAge", - "table": "relativeage", - "tableId": 1020, - "system": false, - "idColumn": "RelativeAgeID", - "idFieldName": "relativeAgeId", - "fields": [ - { - "name": "ageType", - "column": "AgeType", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 64 - }, - { - "name": "ageUncertainty", - "column": "AgeUncertainty", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "collectionDate", - "column": "CollectionDate", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "date1", - "column": "Date1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "date2", - "column": "Date2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.util.Date" - }, - { - "name": "datingMethod", - "column": "DatingMethod", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 64 - }, - { - "name": "datingMethodRemarks", - "column": "DatingMethodRemarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "number1", - "column": "Number1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "number2", - "column": "Number2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "relativeAgePeriod", - "column": "RelativeAgePeriod", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "text1", - "column": "Text1", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "text2", - "column": "Text2", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "verbatimName", - "column": "VerbatimName", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "verbatimPeriod", - "column": "VerbatimPeriod", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "yesno1", - "column": "YesNo1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "yesno2", - "column": "YesNo2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - } - ], - "relationships": [ - { - "name": "ageName", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "GeologicTimePeriod", - "column": "AgeNameID" - }, - { - "name": "ageNameEnd", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "GeologicTimePeriod", - "column": "AgeNameEndID" - }, - { - "name": "agent1", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "Agent1ID" - }, - { - "name": "agent2", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "Agent2ID" - }, - { - "name": "relativeAgeAttachments", - "type": "one-to-many", - "required": false, - "dependent": true, - "relatedModelName": "RelativeAgeAttachment", - "otherSideName": "relativeAge" - }, - { - "name": "collectionObject", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "CollectionObject", - "column": "CollectionObjectID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.AbsoluteAgeAttachment", - "table": "absoluteageattachment", - "tableId": 1021, - "system": false, - "idColumn": "AbsoluteAgeAttachmentID", - "idFieldName": "absoluteAgeAttachmentId", - "fields": [ - { - "name": "ordinal", - "column": "Ordinal", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "attachment", - "type": "many-to-one", - "required": true, - "dependent": true, - "relatedModelName": "Attachment", - "column": "AttachmentID", - "otherSideName": "absoluteAgeAttachments" - }, - { - "name": "collectionMember", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "Collection", - "column": "CollectionMemberID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "absoluteAge", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "AbsoluteAge", - "column": "AbsoluteAgeID", - "otherSideName": "absoluteAgeAttachments" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.RelativeAgeAttachment", - "table": "relativeageattachment", - "tableId": 1022, - "system": false, - "idColumn": "RelativeAgeAttachmentID", - "idFieldName": "relativeAgeAttachmentId", - "fields": [ - { - "name": "ordinal", - "column": "Ordinal", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "attachment", - "type": "many-to-one", - "required": true, - "dependent": true, - "relatedModelName": "Attachment", - "column": "AttachmentID", - "otherSideName": "relativeAgeAttachments" - }, - { - "name": "collectionMember", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "Collection", - "column": "CollectionMemberID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "relativeAge", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "RelativeAge", - "column": "RelativeAgeID", - "otherSideName": "relativeAgeAttachments" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.AbsoluteAgeCitation", - "table": "absoluteagecitation", - "tableId": 1023, - "system": false, - "idColumn": "AbsoluteAgeCitationID", - "idFieldName": "absoluteAgeCitationId", - "fields": [ - { - "name": "figureNumber", - "column": "FigureNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "isFigured", - "column": "IsFigured", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "pageNumber", - "column": "PageNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "plateNumber", - "column": "PlateNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "absoluteAge", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "AbsoluteAge", - "column": "AbsoluteAgeID" - }, - { - "name": "collectionMember", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "Collection", - "column": "CollectionMemberID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "referenceWork", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "ReferenceWork", - "column": "ReferenceWorkID" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.RelativeAgeCitation", - "table": "relativeagecitation", - "tableId": 1024, - "system": false, - "idColumn": "RelativeAgeCitationID", - "idFieldName": "relativeAgeCitationId", - "fields": [ - { - "name": "figureNumber", - "column": "FigureNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "isFigured", - "column": "IsFigured", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "pageNumber", - "column": "PageNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "plateNumber", - "column": "PlateNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 50 - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "collectionMember", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "Collection", - "column": "CollectionMemberID" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "referenceWork", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "ReferenceWork", - "column": "ReferenceWorkID" - }, - { - "name": "relativeAge", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "RelativeAge", - "column": "RelativeAgeID" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.TectonicUnitTreeDef", - "table": "tectonicunittreedef", - "tableId": 1025, - "system": false, - "idColumn": "TectonicUnitTreeDefID", - "idFieldName": "tectonicUnitTreeDefId", - "fields": [ - { - "name": "fullNameDirection", - "column": "FullNameDirection", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "name", - "column": "Name", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "discipline", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "Discipline", - "column": "DisciplineID", - "otherSideName": "tectonicUnitTreeDefs" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "treeDefItems", - "type": "one-to-many", - "required": false, - "dependent": true, - "relatedModelName": "TectonicUnitTreeDefItem", - "otherSideName": "treeDef" - }, - { - "name": "treeEntries", - "type": "one-to-many", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnit", - "otherSideName": "definition" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.TectonicUnitTreeDefItem", - "table": "tectonicunittreedefitem", - "tableId": 1026, - "system": false, - "idColumn": "TectonicUnitTreeDefItemID", - "idFieldName": "tectonicUnitTreeDefItemId", - "fields": [ - { - "name": "fullNameSeparator", - "column": "FullNameSeparator", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "isEnforced", - "column": "IsEnforced", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "isInFullName", - "column": "IsInFullName", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "name", - "column": "Name", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "rankId", - "column": "RankID", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.Integer" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "textAfter", - "column": "TextAfter", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "textBefore", - "column": "TextBefore", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "title", - "column": "Title", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - } - ], - "relationships": [ - { - "name": "children", - "type": "one-to-many", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDefItem", - "otherSideName": "parent" - }, - { - "name": "createdbyagent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "parent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDefItem", - "column": "ParentItemID", - "otherSideName": "children" - }, - { - "name": "treeDef", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDef", - "column": "TectonicUnitTreeDefID", - "otherSideName": "treeDefItems" - }, - { - "name": "treeEntries", - "type": "one-to-many", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnit", - "otherSideName": "definitionItem" - } - ], - "fieldAliases": [] - }, - { - "classname": "edu.ku.brc.specify.datamodel.TectonicUnit", - "table": "tectonicunit", - "tableId": 1027, - "system": false, - "idColumn": "TectonicUnitID", - "idFieldName": "tectonicUnitId", - "fields": [ - { - "name": "fullName", - "column": "FullName", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "guid", - "column": "GUID", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 128 - }, - { - "name": "highestChildNodeNumber", - "column": "HighestChildNodeNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "isAccepted", - "column": "IsAccepted", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.Boolean" - }, - { - "name": "name", - "column": "Name", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "nodeNumber", - "column": "NodeNumber", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "number1", - "column": "Number1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "number2", - "column": "Number2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.math.BigDecimal" - }, - { - "name": "rankId", - "column": "RankID", - "indexed": false, - "unique": false, - "required": true, - "type": "java.lang.Integer" - }, - { - "name": "remarks", - "column": "Remarks", - "indexed": false, - "unique": false, - "required": false, - "type": "text" - }, - { - "name": "text1", - "column": "Text1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "text2", - "column": "Text2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.String", - "length": 255 - }, - { - "name": "timestampCreated", - "column": "TimestampCreated", - "indexed": false, - "unique": false, - "required": true, - "type": "java.sql.Timestamp" - }, - { - "name": "timestampModified", - "column": "TimestampModified", - "indexed": false, - "unique": false, - "required": false, - "type": "java.sql.Timestamp" - }, - { - "name": "version", - "column": "Version", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Integer" - }, - { - "name": "yesno1", - "column": "YesNo1", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - }, - { - "name": "yesno2", - "column": "YesNo2", - "indexed": false, - "unique": false, - "required": false, - "type": "java.lang.Boolean" - } - ], - "relationships": [ - { - "name": "acceptedChildren", - "type": "one-to-many", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnit", - "otherSideName": "acceptedTectonicUnit" - }, - { - "name": "acceptedTectonicUnit", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "TectonicUnit", - "column": "AcceptedID", - "otherSideName": "acceptedChildren" - }, - { - "name": "createdByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "CreatedByAgentID" - }, - { - "name": "modifiedByAgent", - "type": "many-to-one", - "required": false, - "dependent": false, - "relatedModelName": "Agent", - "column": "ModifiedByAgentID" - }, - { - "name": "parent", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "TectonicUnit", - "column": "ParentID" - }, - { - "name": "definition", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDef", - "column": "TectonicUnitTreeDefID", - "otherSideName": "treeEntries" - }, - { - "name": "definitionItem", - "type": "many-to-one", - "required": true, - "dependent": false, - "relatedModelName": "TectonicUnitTreeDefItem", - "column": "TectonicUnitTreeDefItemID", - "otherSideName": "treeEntries" - } - ], - "fieldAliases": [] } ] \ No newline at end of file From b4bc8ec80e90476a2d3e69abe131e5f59bb96f3e Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 1 Nov 2024 10:16:22 -0400 Subject: [PATCH 130/132] Revert "Merge remote-tracking branch 'origin/issue-5185' into issue-5246" This reverts commit 9c00ccac945133f122d0684c7cec27e9de5f52d5, reversing changes made to aac3197eec6b6fe1d32c99141bc866187c9ddfc8. --- specifyweb/businessrules/rules/cojo_rules.py | 12 +- .../lib/components/Attachments/index.tsx | 4 +- .../__snapshots__/specifyTable.test.ts.snap | 105 ----- .../DataModel/__tests__/businessRules.test.ts | 5 - .../DataModel/__tests__/collectionApi.test.ts | 436 +----------------- .../DataModel/__tests__/resource.test.ts | 2 - .../DataModel/__tests__/resourceApi.test.ts | 113 +---- .../DataModel/__tests__/specifyTable.test.ts | 102 +++- .../components/DataModel/businessRuleDefs.ts | 1 + .../lib/components/DataModel/businessRules.ts | 1 - .../lib/components/DataModel/collection.ts | 7 +- .../lib/components/DataModel/collectionApi.ts | 198 ++------ .../lib/components/DataModel/helpers.ts | 14 +- .../lib/components/DataModel/legacyTypes.ts | 8 +- .../lib/components/DataModel/resourceApi.ts | 277 +++-------- .../lib/components/DataModel/saveBlockers.tsx | 44 +- .../lib/components/DataModel/schemaExtras.ts | 8 - .../lib/components/DataModel/scoping.ts | 36 +- .../lib/components/DataModel/specifyTable.ts | 29 +- .../js_src/lib/components/DataModel/types.ts | 2 - .../lib/components/FormCells/COJODialog.tsx | 158 ------- .../lib/components/FormCells/FormTable.tsx | 99 ++-- .../FormCells/FormTableCollection.tsx | 14 +- .../lib/components/FormEditor/viewSpec.ts | 38 +- .../js_src/lib/components/FormParse/fields.ts | 10 - .../FormSliders/IntegratedRecordSelector.tsx | 106 +---- .../components/FormSliders/RecordSelector.tsx | 61 ++- .../RecordSelectorFromCollection.tsx | 24 +- .../lib/components/Forms/DeleteButton.tsx | 14 - .../js_src/lib/components/Forms/SubView.tsx | 301 +++++++----- .../__tests__/treeRanks.test.ts | 16 +- .../lib/components/QueryComboBox/index.tsx | 2 +- .../lib/components/SearchDialog/index.tsx | 50 +- .../WorkBench/useDisambiguationDialog.tsx | 3 +- .../js_src/lib/hooks/useCollection.tsx | 227 +++------ .../lib/hooks/useSerializedCollection.tsx | 89 ---- .../frontend/js_src/lib/localization/forms.ts | 3 - .../tests/ajax/static/api/specify_trees.json | 2 + specifyweb/permissions/permissions.py | 1 + specifyweb/specify/api.py | 155 ++----- specifyweb/specify/datamodel.py | 1 - specifyweb/specify/tests/test_api.py | 169 +------ 42 files changed, 691 insertions(+), 2256 deletions(-) delete mode 100644 specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx delete mode 100644 specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx diff --git a/specifyweb/businessrules/rules/cojo_rules.py b/specifyweb/businessrules/rules/cojo_rules.py index d98ce0310b7..d4925fa5932 100644 --- a/specifyweb/businessrules/rules/cojo_rules.py +++ b/specifyweb/businessrules/rules/cojo_rules.py @@ -24,14 +24,4 @@ def cojo_pre_save(cojo): (Collectionobjectgroupjoin.objects .filter(parentcog=cojo.parentcog) .update(issubstrate=False)) - -@orm_signal_handler('post_save', 'Collectionobjectgroupjoin') -def cojo_post_save(cojo): - if cojo.childcog is not None: - cojo.childcog.parentcojo = cojo - cojo.childcog.save() - -@orm_signal_handler('pre_delete', 'Collectionobjectgroupjoin') -def cojo_pre_delete(cojo): - cojo.childcog.parentcojo = None - cojo.childcog.save() \ No newline at end of file + \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx index 8090f1ee5d2..baef46664ac 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Attachments/index.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom'; import { useAsyncState, usePromise } from '../../hooks/useAsyncState'; import { useCachedState } from '../../hooks/useCachedState'; -import { useSerializedCollection } from '../../hooks/useSerializedCollection'; +import { useCollection } from '../../hooks/useCollection'; import { attachmentsText } from '../../localization/attachments'; import { commonText } from '../../localization/common'; import { schemaText } from '../../localization/schema'; @@ -125,7 +125,7 @@ function Attachments({ 'scale' ); - const [collection, setCollection, fetchMore] = useSerializedCollection( + const [collection, setCollection, fetchMore] = useCollection( React.useCallback( async (offset) => fetchCollection( diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap index 58a082a2950..d0ab68e2a81 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/__snapshots__/specifyTable.test.ts.snap @@ -27,7 +27,6 @@ exports[`fields are loaded 1`] = ` "[literalField CollectionObject.text2]", "[literalField CollectionObject.inventoryDate]", "[literalField CollectionObject.inventoryDatePrecision]", - "[literalField CollectionObject.isMemberOfCOG]", "[literalField CollectionObject.modifier]", "[literalField CollectionObject.name]", "[literalField CollectionObject.notifications]", @@ -103,109 +102,6 @@ exports[`fields are loaded 1`] = ` ] `; -exports[`indexed fields are loaded 1`] = ` -{ - "accession": "[relationship CollectionObject.accession]", - "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", - "age": "[literalField CollectionObject.age]", - "agent1": "[relationship CollectionObject.agent1]", - "altCatalogNumber": "[literalField CollectionObject.altCatalogNumber]", - "appraisal": "[relationship CollectionObject.appraisal]", - "availability": "[literalField CollectionObject.availability]", - "catalogNumber": "[literalField CollectionObject.catalogNumber]", - "catalogedDate": "[literalField CollectionObject.catalogedDate]", - "catalogedDatePrecision": "[literalField CollectionObject.catalogedDatePrecision]", - "catalogedDateVerbatim": "[literalField CollectionObject.catalogedDateVerbatim]", - "cataloger": "[relationship CollectionObject.cataloger]", - "cojo": "[relationship CollectionObject.cojo]", - "collectingEvent": "[relationship CollectionObject.collectingEvent]", - "collection": "[relationship CollectionObject.collection]", - "collectionMemberId": "[literalField CollectionObject.collectionMemberId]", - "collectionObjectAttachments": "[relationship CollectionObject.collectionObjectAttachments]", - "collectionObjectAttribute": "[relationship CollectionObject.collectionObjectAttribute]", - "collectionObjectAttrs": "[relationship CollectionObject.collectionObjectAttrs]", - "collectionObjectCitations": "[relationship CollectionObject.collectionObjectCitations]", - "collectionObjectProperties": "[relationship CollectionObject.collectionObjectProperties]", - "collectionObjectType": "[relationship CollectionObject.collectionObjectType]", - "conservDescriptions": "[relationship CollectionObject.conservDescriptions]", - "container": "[relationship CollectionObject.container]", - "containerOwner": "[relationship CollectionObject.containerOwner]", - "countAmt": "[literalField CollectionObject.countAmt]", - "createdByAgent": "[relationship CollectionObject.createdByAgent]", - "currentDetermination": "[relationship CollectionObject.currentDetermination]", - "date1": "[literalField CollectionObject.date1]", - "date1Precision": "[literalField CollectionObject.date1Precision]", - "deaccessioned": "[literalField CollectionObject.deaccessioned]", - "description": "[literalField CollectionObject.description]", - "determinations": "[relationship CollectionObject.determinations]", - "dnaSequences": "[relationship CollectionObject.dnaSequences]", - "embargoAuthority": "[relationship CollectionObject.embargoAuthority]", - "embargoReason": "[literalField CollectionObject.embargoReason]", - "embargoReleaseDate": "[literalField CollectionObject.embargoReleaseDate]", - "embargoReleaseDatePrecision": "[literalField CollectionObject.embargoReleaseDatePrecision]", - "embargoStartDate": "[literalField CollectionObject.embargoStartDate]", - "embargoStartDatePrecision": "[literalField CollectionObject.embargoStartDatePrecision]", - "exsiccataItems": "[relationship CollectionObject.exsiccataItems]", - "fieldNotebookPage": "[relationship CollectionObject.fieldNotebookPage]", - "fieldNumber": "[literalField CollectionObject.fieldNumber]", - "guid": "[literalField CollectionObject.guid]", - "integer1": "[literalField CollectionObject.integer1]", - "integer2": "[literalField CollectionObject.integer2]", - "inventorizedBy": "[relationship CollectionObject.inventorizedBy]", - "inventoryDate": "[literalField CollectionObject.inventoryDate]", - "inventoryDatePrecision": "[literalField CollectionObject.inventoryDatePrecision]", - "isMemberOfCOG": "[literalField CollectionObject.isMemberOfCOG]", - "leftSideRels": "[relationship CollectionObject.leftSideRels]", - "modifiedByAgent": "[relationship CollectionObject.modifiedByAgent]", - "modifier": "[literalField CollectionObject.modifier]", - "name": "[literalField CollectionObject.name]", - "notifications": "[literalField CollectionObject.notifications]", - "number1": "[literalField CollectionObject.number1]", - "number2": "[literalField CollectionObject.number2]", - "numberOfDuplicates": "[literalField CollectionObject.numberOfDuplicates]", - "objectCondition": "[literalField CollectionObject.objectCondition]", - "ocr": "[literalField CollectionObject.ocr]", - "otherIdentifiers": "[relationship CollectionObject.otherIdentifiers]", - "paleoContext": "[relationship CollectionObject.paleoContext]", - "preparations": "[relationship CollectionObject.preparations]", - "projectNumber": "[literalField CollectionObject.projectNumber]", - "projects": "[relationship CollectionObject.projects]", - "remarks": "[literalField CollectionObject.remarks]", - "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", - "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", - "reservedText": "[literalField CollectionObject.reservedText]", - "reservedText2": "[literalField CollectionObject.reservedText2]", - "reservedText3": "[literalField CollectionObject.reservedText3]", - "restrictions": "[literalField CollectionObject.restrictions]", - "rightSideRels": "[relationship CollectionObject.rightSideRels]", - "sgrStatus": "[literalField CollectionObject.sgrStatus]", - "text1": "[literalField CollectionObject.text1]", - "text2": "[literalField CollectionObject.text2]", - "text3": "[literalField CollectionObject.text3]", - "text4": "[literalField CollectionObject.text4]", - "text5": "[literalField CollectionObject.text5]", - "text6": "[literalField CollectionObject.text6]", - "text7": "[literalField CollectionObject.text7]", - "text8": "[literalField CollectionObject.text8]", - "timestampCreated": "[literalField CollectionObject.timestampCreated]", - "timestampModified": "[literalField CollectionObject.timestampModified]", - "totalCountAmt": "[literalField CollectionObject.totalCountAmt]", - "totalValue": "[literalField CollectionObject.totalValue]", - "treatmentEvents": "[relationship CollectionObject.treatmentEvents]", - "uniqueIdentifier": "[literalField CollectionObject.uniqueIdentifier]", - "version": "[literalField CollectionObject.version]", - "visibility": "[literalField CollectionObject.visibility]", - "visibilitySetBy": "[relationship CollectionObject.visibilitySetBy]", - "voucherRelationships": "[relationship CollectionObject.voucherRelationships]", - "yesNo1": "[literalField CollectionObject.yesNo1]", - "yesNo2": "[literalField CollectionObject.yesNo2]", - "yesNo3": "[literalField CollectionObject.yesNo3]", - "yesNo4": "[literalField CollectionObject.yesNo4]", - "yesNo5": "[literalField CollectionObject.yesNo5]", - "yesNo6": "[literalField CollectionObject.yesNo6]", -} -`; - exports[`literal fields are loaded 1`] = ` [ "[literalField CollectionObject.actualTotalCountAmt]", @@ -233,7 +129,6 @@ exports[`literal fields are loaded 1`] = ` "[literalField CollectionObject.text2]", "[literalField CollectionObject.inventoryDate]", "[literalField CollectionObject.inventoryDatePrecision]", - "[literalField CollectionObject.isMemberOfCOG]", "[literalField CollectionObject.modifier]", "[literalField CollectionObject.name]", "[literalField CollectionObject.notifications]", diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 87de7d4969b..61c84052045 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -306,11 +306,6 @@ describe('uniqueness rules', () => { ]); }); - overrideAjax(getResourceApiUrl('Agent', 1), { - id: 1, - resource_uri: getResourceApiUrl('Agent', 1), - }); - test('rule with local collection', async () => { const accessionId = 1; const accession = new tables.Accession.Resource({ diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts index 7caab498f0d..db1747601f7 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collectionApi.test.ts @@ -1,33 +1,30 @@ import { overrideAjax } from '../../../tests/ajax'; import { requireContext } from '../../../tests/helpers'; import { overwriteReadOnly } from '../../../utils/types'; -import type { CollectionFetchFilters } from '../collection'; -import { DEFAULT_FETCH_LIMIT } from '../collection'; -import type { AnySchema } from '../helperTypes'; import { getResourceApiUrl } from '../resource'; import type { Collection } from '../specifyTable'; import { tables } from '../tables'; -import type { Accession, Agent, CollectionObject } from '../types'; +import type { Accession, Agent } from '../types'; requireContext(); -describe('LazyCollection', () => { - const secondAccessionUrl = getResourceApiUrl('Accession', 12); - const accessionId = 11; - const accessionUrl = getResourceApiUrl('Accession', accessionId); - const accessionNumber = '2011-IC-116'; - const accessionsResponse = [ - { - resource_uri: accessionUrl, - id: 11, - accessionnumber: accessionNumber, - }, - { - resource_uri: secondAccessionUrl, - id: 12, - }, - ]; +const secondAccessionUrl = getResourceApiUrl('Accession', 12); +const accessionId = 11; +const accessionUrl = getResourceApiUrl('Accession', accessionId); +const accessionNumber = '2011-IC-116'; +const accessionsResponse = [ + { + resource_uri: accessionUrl, + id: 11, + accessionnumber: accessionNumber, + }, + { + resource_uri: secondAccessionUrl, + id: 12, + }, +]; +describe('LazyCollection', () => { overrideAjax( '/api/specify/accession/?domainfilter=false&addressofrecord=4&offset=0', { @@ -74,402 +71,3 @@ describe('LazyCollection', () => { expect(collection.toJSON()).toEqual(accessionsResponse); }); }); - -describe('Independent Collection', () => { - const collectionObjectsResponse = Array.from({ length: 41 }, (_, index) => ({ - id: index + 1, - resource_uri: getResourceApiUrl('CollectionObject', index + 1), - })); - - overrideAjax( - '/api/specify/collectionobject/?domainfilter=false&accession=1&offset=0', - { - objects: collectionObjectsResponse.slice(0, DEFAULT_FETCH_LIMIT), - meta: { - limit: DEFAULT_FETCH_LIMIT, - total_count: collectionObjectsResponse.length, - }, - } - ); - - overrideAjax( - `/api/specify/collectionobject/?domainfilter=false&accession=1&offset=${DEFAULT_FETCH_LIMIT}`, - { - objects: collectionObjectsResponse.slice( - DEFAULT_FETCH_LIMIT, - DEFAULT_FETCH_LIMIT * 2 - ), - meta: { - limit: DEFAULT_FETCH_LIMIT, - total_count: collectionObjectsResponse.length, - }, - } - ); - - overrideAjax( - `/api/specify/collectionobject/?domainfilter=false&accession=1&offset=${ - DEFAULT_FETCH_LIMIT * 2 - }`, - { - objects: collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT * 2), - meta: { - limit: DEFAULT_FETCH_LIMIT, - total_count: collectionObjectsResponse.length, - }, - } - ); - - overrideAjax( - '/api/specify/collectionobject/?domainfilter=false&accession=1&offset=20&limit=0', - { - objects: collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT), - meta: { - limit: 0, - total_count: collectionObjectsResponse.length, - }, - } - ); - - test('lazily fetched', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - expect(collection._totalCount).toBe(collectionObjectsResponse.length); - expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse - .slice(0, DEFAULT_FETCH_LIMIT) - .map(({ id }) => id) - ); - - await collection.fetch(); - expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT * 2); - expect( - collection.models - .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) - .map(({ id }) => id) - ).toStrictEqual( - collectionObjectsResponse - .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) - .map(({ id }) => id) - ); - - await collection.fetch(); - // eslint-disable-next-line jest/prefer-to-have-length - expect(collection.length).toBe(collection._totalCount); - }); - - test('specified offset', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch({ - offset: DEFAULT_FETCH_LIMIT, - }); - expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse - .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) - .map(({ id }) => id) - ); - }); - - test('reset', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch({ - offset: DEFAULT_FETCH_LIMIT, - limit: 0, - }); - expect(collection).toHaveLength( - collectionObjectsResponse.length - DEFAULT_FETCH_LIMIT - ); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse.slice(DEFAULT_FETCH_LIMIT).map(({ id }) => id) - ); - await collection.fetch({ - reset: true, - offset: 0, - } as CollectionFetchFilters); - expect(collection).toHaveLength(DEFAULT_FETCH_LIMIT); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse - .slice(0, DEFAULT_FETCH_LIMIT) - .map(({ id }) => id) - ); - }); - - test('removed objects not refetched', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - const collectionObjectsToRemove = collection.models - .slice(0, 5) - .map((collectionObject) => ({ ...collectionObject })); - collectionObjectsToRemove.forEach((collectionObject) => - collection.remove(collectionObject) - ); - await collection.fetch({ offset: 0 }); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse - .slice(5, DEFAULT_FETCH_LIMIT) - .map(({ id }) => id) - ); - }); - - test('offset adjusted when all models removed', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - const collectionObjectsToRemove = collection.models.map( - (collectionObject) => ({ ...collectionObject }) - ); - collectionObjectsToRemove.forEach((collectionObject) => - collection.remove(collectionObject) - ); - expect(collection.getFetchOffset()).toBe(DEFAULT_FETCH_LIMIT); - await collection.fetch(); - expect(collection.models.map(({ id }) => id)).toStrictEqual( - collectionObjectsResponse - .slice(DEFAULT_FETCH_LIMIT, DEFAULT_FETCH_LIMIT * 2) - .map(({ id }) => id) - ); - }); - - test('on resource change event', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - - expect(collection._totalCount).toBe(collectionObjectsResponse.length); - - collection.models[0].set('text1', 'someValue'); - expect( - Object.values(collection.updated ?? {}).map((resource) => - typeof resource === 'string' ? resource : resource.toJSON() - ) - ).toStrictEqual([ - { - id: 1, - resource_uri: '/api/specify/collectionobject/1/', - collectionobjecttype: '/api/specify/collectionobjecttype/1/', - text1: 'someValue', - }, - ]); - }); - - overrideAjax('/api/specify/accession/1/', { - id: 1, - resource_uri: getResourceApiUrl('Accession', 1), - }); - - overrideAjax('/api/specify/collectionobject/1/', { - id: 1, - resource_uri: getResourceApiUrl('CollectionObject', 1), - }); - - test('on change toOne', async () => { - const collectionObject = new tables.CollectionObject.Resource({ id: 1 }); - - const collection = new tables.Accession.IndependentCollection({ - related: collectionObject, - field: tables.Accession.strictGetRelationship('collectionObjects'), - }) as Collection; - - const rawAccession = new tables.Accession.Resource({ id: 1 }); - const accession = await rawAccession.fetch(); - - expect(collectionObject.get('accession')).toBeUndefined(); - collection.add(accession); - expect(collection.updated?.[accession.cid]).toBe( - getResourceApiUrl('Accession', 1) - ); - accession.set('accessionNumber', '2011-IC-116'); - expect(collection.updated?.[accession.cid]).toBe(accession); - expect(collectionObject.get('accession')).toBe( - getResourceApiUrl('Accession', 1) - ); - }); - - test('on add event', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - - const newCollectionObjects = [ - new tables.CollectionObject.Resource(), - new tables.CollectionObject.Resource({ id: 100 }), - ]; - collection.add(newCollectionObjects); - expect(collection._totalCount).toBe( - collectionObjectsResponse.length + newCollectionObjects.length - ); - expect(Object.keys(collection.updated ?? {})).toStrictEqual( - newCollectionObjects.map(({ cid }) => cid) - ); - newCollectionObjects.forEach((collectionObject) => { - const updatedEntry = collection.updated?.[collectionObject.cid]; - expect(updatedEntry).toBe( - collectionObject.isNew() ? collectionObject : collectionObject.url() - ); - }); - }); - test('on remove event', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - - const collection = await rawCollection.fetch(); - - const collectionObjectsToRemove = collection.models.slice(0, 3); - collectionObjectsToRemove.forEach((collectionObject) => - collection.remove(collectionObject) - ); - expect(collection._totalCount).toBe( - collectionObjectsResponse.length - collectionObjectsToRemove.length - ); - expect(Array.from(collection.removed ?? [])).toStrictEqual( - collectionObjectsToRemove.map((resource) => resource.get('resource_uri')) - ); - }); - test('removed and updated modify eachother', () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const collection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }) as Collection; - const collectionObject = new tables.CollectionObject.Resource({ id: 1 }); - collection.add(collectionObject); - expect(collection.updated).toStrictEqual({ - [collectionObject.cid]: collectionObject.url(), - }); - collection.remove(collectionObject); - expect(collection.removed).toStrictEqual(new Set([collectionObject.url()])); - expect(collection.updated).toStrictEqual({}); - collection.add(collectionObject); - expect(collection.updated).toStrictEqual({ - [collectionObject.cid]: collectionObject.url(), - }); - expect(collection.removed).toStrictEqual(new Set()); - }); - - test('success options respected', async () => { - const accession = new tables.Accession.Resource(); - - expect(accession.isNew()).toBe(true); - - const collection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }) as Collection; - - await collection.fetch({ - success: (collection) => { - collection.add(new tables.CollectionObject.Resource()); - }, - } as CollectionFetchFilters); - expect(collection.models).toHaveLength(1); - }); - - overrideAjax('/api/specify/collectionobject/200/', { - id: 200, - resource_uri: getResourceApiUrl('CollectionObject', 200), - }); - - test('toApiJSON', async () => { - const accession = new tables.Accession.Resource({ - id: 1, - }); - - const rawCollection = new tables.CollectionObject.IndependentCollection({ - related: accession, - field: tables.CollectionObject.strictGetRelationship('accession'), - }); - const collection = await rawCollection.fetch(); - expect(collection.toApiJSON()).toStrictEqual({ - update: [], - remove: [], - }); - const collectionObjectsToRemove = collection.models - .slice(1, 4) - .map((collectionObject) => collectionObject); - - collectionObjectsToRemove.forEach((collectionObject) => { - collection.remove(collectionObject); - }); - - const collectionObjectsToAdd = [ - new tables.CollectionObject.Resource({ id: 200 }), - new tables.CollectionObject.Resource({ text1: 'someValue' }), - ]; - collection.add(collectionObjectsToAdd); - collection.models[0].set('catalogNumber', '000000001'); - - expect(collection.toApiJSON()).toStrictEqual({ - remove: collectionObjectsToRemove.map((collectionObject) => - collectionObject.get('resource_uri') - ), - update: [ - '/api/specify/collectionobject/200/', - collection.models.at(-1), - collection.models[0], - ], - }); - }); -}); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts index 787ed89f2cf..733cc18e7bd 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resource.test.ts @@ -295,7 +295,6 @@ test('getFieldsToNotClone', () => { 'catalogNumber', 'timestampModified', 'guid', - 'isMemberOfCOG', 'timestampCreated', 'totalCountAmt', 'uniqueIdentifier', @@ -310,7 +309,6 @@ test('getFieldsToNotClone', () => { 'catalogNumber', 'timestampModified', 'guid', - 'isMemberOfCOG', 'text1', 'timestampCreated', 'totalCountAmt', diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index de9ab69cf03..5c765575805 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -2,7 +2,6 @@ import { overrideAjax } from '../../../tests/ajax'; import { requireContext } from '../../../tests/helpers'; import type { RA } from '../../../utils/types'; import { replaceItem } from '../../../utils/utils'; -import { addMissingFields } from '../addMissingFields'; import type { SerializedRecord } from '../helperTypes'; import { getResourceApiUrl } from '../resource'; import { tables } from '../tables'; @@ -22,11 +21,7 @@ const collectionObjectUrl = getResourceApiUrl( ); const accessionId = 11; const accessionUrl = getResourceApiUrl('Accession', accessionId); -const collectingEventId = 8868; -const collectingEventUrl = getResourceApiUrl( - 'CollectingEvent', - collectingEventId -); +const collectingEventUrl = getResourceApiUrl('CollectingEvent', 8868); const determinationUrl = getResourceApiUrl('Determination', 123); const determinationsResponse: RA>> = [ @@ -70,12 +65,9 @@ const accessionResponse = { }; overrideAjax(accessionUrl, accessionResponse); -const collectingEventText = 'testCollectingEvent'; - const collectingEventResponse = { resource_uri: collectingEventUrl, - text1: collectingEventText, - id: collectingEventId, + id: 8868, }; overrideAjax(collectingEventUrl, collectingEventResponse); @@ -181,27 +173,14 @@ describe('rgetCollection', () => { expect(agents.models).toHaveLength(0); }); - test('repeated calls for independent return same object', async () => { + test('repeated calls for independent return different object', async () => { const resource = new tables.CollectionObject.Resource({ id: collectionObjectId, }); const firstCollectingEvent = await resource.rgetPromise('collectingEvent'); const secondCollectingEvent = await resource.rgetPromise('collectingEvent'); expect(firstCollectingEvent?.toJSON()).toEqual(collectingEventResponse); - expect(firstCollectingEvent).toBe(secondCollectingEvent); - }); - - test('call for independent refetches related', async () => { - const resource = new tables.CollectionObject.Resource({ - id: collectionObjectId, - }); - const newCollectingEvent = new tables.CollectingEvent.Resource({ - id: collectingEventId, - text1: 'someOtherText', - }); - resource.set('collectingEvent', newCollectingEvent); - const firstCollectingEvent = await resource.rgetPromise('collectingEvent'); - expect(firstCollectingEvent?.get('text1')).toEqual(collectingEventText); + expect(firstCollectingEvent).not.toBe(secondCollectingEvent); }); test('repeated calls for dependent return same object', async () => { @@ -220,90 +199,6 @@ describe('rgetCollection', () => { // TEST: add dependent and independent tests for all relationship types (and zero-to-one) }); -describe('eventHandlerForToMany', () => { - test('saverequired', () => { - const resource = new tables.CollectionObject.Resource( - addMissingFields('CollectionObject', { - preparations: [ - { - id: 1, - _tableName: 'Preparation', - }, - ], - }) - ); - const testFunction = jest.fn(); - resource.on('saverequired', testFunction); - expect(testFunction).toHaveBeenCalledTimes(0); - expect(resource.needsSaved).toBe(false); - resource - .getDependentResource('preparations') - ?.models[0].set('text1', 'helloWorld'); - - expect(resource.needsSaved).toBe(true); - expect(testFunction).toHaveBeenCalledTimes(1); - }); - test('changing collection propagates to related', () => { - const resource = new tables.CollectionObject.Resource( - addMissingFields('CollectionObject', { - preparations: [ - { - id: 1, - _tableName: 'Preparation', - }, - ], - }) - ); - const onResourceChange = jest.fn(); - const onPrepChange = jest.fn(); - const onPrepAdd = jest.fn(); - const onPrepRemoval = jest.fn(); - resource.on('change', onResourceChange); - resource.on('change:preparations', onPrepChange); - resource.on('add:preparations', onPrepAdd); - resource.on('remove:preparations', onPrepRemoval); - - resource - .getDependentResource('preparations') - ?.models[0].set('text1', 'helloWorld', { silent: false }); - expect(onResourceChange).toHaveBeenCalledWith( - resource, - resource.getDependentResource('preparations') - ); - expect(onPrepChange).toHaveBeenCalledWith( - resource.getDependentResource('preparations')?.models[0], - { silent: false } - ); - const newPrep = new tables.Preparation.Resource({ - barCode: 'test', - }); - resource.getDependentResource('preparations')?.add(newPrep); - expect(onPrepAdd).toHaveBeenCalledWith( - newPrep, - resource.getDependentResource('preparations'), - {} - ); - resource.getDependentResource('preparations')?.remove(newPrep); - expect(onPrepRemoval).toHaveBeenCalledWith( - newPrep, - resource.getDependentResource('preparations'), - { index: 1 } - ); - - expect(onResourceChange).toHaveBeenCalledTimes(3); - - resource.set('determinations', [ - addMissingFields('Determination', { - taxon: getResourceApiUrl('Taxon', 1), - }), - ]); - expect(onResourceChange).toHaveBeenCalledTimes(4); - expect(onPrepChange).toHaveBeenCalledTimes(1); - expect(onPrepAdd).toHaveBeenCalledTimes(1); - expect(onPrepRemoval).toHaveBeenCalledTimes(1); - }); -}); - describe('needsSaved', () => { test('changing field makes needsSaved true', () => { const resource = new tables.CollectionObject.Resource({ diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts index 07b923db338..4679ddeb229 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/specifyTable.test.ts @@ -364,4 +364,104 @@ test('tableScoping', () => ).toMatchSnapshot()); test('indexed fields are loaded', () => - expect(tables.CollectionObject.field).toMatchSnapshot()); + expect(tables.CollectionObject.field).toMatchInlineSnapshot(` + { + "accession": "[relationship CollectionObject.accession]", + "actualTotalCountAmt": "[literalField CollectionObject.actualTotalCountAmt]", + "age": "[literalField CollectionObject.age]", + "agent1": "[relationship CollectionObject.agent1]", + "altCatalogNumber": "[literalField CollectionObject.altCatalogNumber]", + "appraisal": "[relationship CollectionObject.appraisal]", + "availability": "[literalField CollectionObject.availability]", + "catalogNumber": "[literalField CollectionObject.catalogNumber]", + "catalogedDate": "[literalField CollectionObject.catalogedDate]", + "catalogedDatePrecision": "[literalField CollectionObject.catalogedDatePrecision]", + "catalogedDateVerbatim": "[literalField CollectionObject.catalogedDateVerbatim]", + "cataloger": "[relationship CollectionObject.cataloger]", + "cojo": "[relationship CollectionObject.cojo]", + "collectingEvent": "[relationship CollectionObject.collectingEvent]", + "collection": "[relationship CollectionObject.collection]", + "collectionMemberId": "[literalField CollectionObject.collectionMemberId]", + "collectionObjectAttachments": "[relationship CollectionObject.collectionObjectAttachments]", + "collectionObjectAttribute": "[relationship CollectionObject.collectionObjectAttribute]", + "collectionObjectAttrs": "[relationship CollectionObject.collectionObjectAttrs]", + "collectionObjectCitations": "[relationship CollectionObject.collectionObjectCitations]", + "collectionObjectProperties": "[relationship CollectionObject.collectionObjectProperties]", + "collectionObjectType": "[relationship CollectionObject.collectionObjectType]", + "conservDescriptions": "[relationship CollectionObject.conservDescriptions]", + "container": "[relationship CollectionObject.container]", + "containerOwner": "[relationship CollectionObject.containerOwner]", + "countAmt": "[literalField CollectionObject.countAmt]", + "createdByAgent": "[relationship CollectionObject.createdByAgent]", + "currentDetermination": "[relationship CollectionObject.currentDetermination]", + "date1": "[literalField CollectionObject.date1]", + "date1Precision": "[literalField CollectionObject.date1Precision]", + "deaccessioned": "[literalField CollectionObject.deaccessioned]", + "description": "[literalField CollectionObject.description]", + "determinations": "[relationship CollectionObject.determinations]", + "dnaSequences": "[relationship CollectionObject.dnaSequences]", + "embargoAuthority": "[relationship CollectionObject.embargoAuthority]", + "embargoReason": "[literalField CollectionObject.embargoReason]", + "embargoReleaseDate": "[literalField CollectionObject.embargoReleaseDate]", + "embargoReleaseDatePrecision": "[literalField CollectionObject.embargoReleaseDatePrecision]", + "embargoStartDate": "[literalField CollectionObject.embargoStartDate]", + "embargoStartDatePrecision": "[literalField CollectionObject.embargoStartDatePrecision]", + "exsiccataItems": "[relationship CollectionObject.exsiccataItems]", + "fieldNotebookPage": "[relationship CollectionObject.fieldNotebookPage]", + "fieldNumber": "[literalField CollectionObject.fieldNumber]", + "guid": "[literalField CollectionObject.guid]", + "integer1": "[literalField CollectionObject.integer1]", + "integer2": "[literalField CollectionObject.integer2]", + "inventorizedBy": "[relationship CollectionObject.inventorizedBy]", + "inventoryDate": "[literalField CollectionObject.inventoryDate]", + "inventoryDatePrecision": "[literalField CollectionObject.inventoryDatePrecision]", + "leftSideRels": "[relationship CollectionObject.leftSideRels]", + "modifiedByAgent": "[relationship CollectionObject.modifiedByAgent]", + "modifier": "[literalField CollectionObject.modifier]", + "name": "[literalField CollectionObject.name]", + "notifications": "[literalField CollectionObject.notifications]", + "number1": "[literalField CollectionObject.number1]", + "number2": "[literalField CollectionObject.number2]", + "numberOfDuplicates": "[literalField CollectionObject.numberOfDuplicates]", + "objectCondition": "[literalField CollectionObject.objectCondition]", + "ocr": "[literalField CollectionObject.ocr]", + "otherIdentifiers": "[relationship CollectionObject.otherIdentifiers]", + "paleoContext": "[relationship CollectionObject.paleoContext]", + "preparations": "[relationship CollectionObject.preparations]", + "projectNumber": "[literalField CollectionObject.projectNumber]", + "projects": "[relationship CollectionObject.projects]", + "remarks": "[literalField CollectionObject.remarks]", + "reservedInteger3": "[literalField CollectionObject.reservedInteger3]", + "reservedInteger4": "[literalField CollectionObject.reservedInteger4]", + "reservedText": "[literalField CollectionObject.reservedText]", + "reservedText2": "[literalField CollectionObject.reservedText2]", + "reservedText3": "[literalField CollectionObject.reservedText3]", + "restrictions": "[literalField CollectionObject.restrictions]", + "rightSideRels": "[relationship CollectionObject.rightSideRels]", + "sgrStatus": "[literalField CollectionObject.sgrStatus]", + "text1": "[literalField CollectionObject.text1]", + "text2": "[literalField CollectionObject.text2]", + "text3": "[literalField CollectionObject.text3]", + "text4": "[literalField CollectionObject.text4]", + "text5": "[literalField CollectionObject.text5]", + "text6": "[literalField CollectionObject.text6]", + "text7": "[literalField CollectionObject.text7]", + "text8": "[literalField CollectionObject.text8]", + "timestampCreated": "[literalField CollectionObject.timestampCreated]", + "timestampModified": "[literalField CollectionObject.timestampModified]", + "totalCountAmt": "[literalField CollectionObject.totalCountAmt]", + "totalValue": "[literalField CollectionObject.totalValue]", + "treatmentEvents": "[relationship CollectionObject.treatmentEvents]", + "uniqueIdentifier": "[literalField CollectionObject.uniqueIdentifier]", + "version": "[literalField CollectionObject.version]", + "visibility": "[literalField CollectionObject.visibility]", + "visibilitySetBy": "[relationship CollectionObject.visibilitySetBy]", + "voucherRelationships": "[relationship CollectionObject.voucherRelationships]", + "yesNo1": "[literalField CollectionObject.yesNo1]", + "yesNo2": "[literalField CollectionObject.yesNo2]", + "yesNo3": "[literalField CollectionObject.yesNo3]", + "yesNo4": "[literalField CollectionObject.yesNo4]", + "yesNo5": "[literalField CollectionObject.yesNo5]", + "yesNo6": "[literalField CollectionObject.yesNo6]", + } + `)); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 57dcdd0c6c0..bcf469717ff 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -141,6 +141,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }, }, }, + CollectionObject: { customInit: (collectionObject: SpecifyResource): void => { const ceField = collectionObject.specifyTable.getField('collectingEvent'); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts index 1d674cda2a0..01c162c429e 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts @@ -56,7 +56,6 @@ export class BusinessRuleManager { if (isTreeResource(this.resource as SpecifyResource)) initializeTreeRecord(this.resource as SpecifyResource); - // REFACTOR: use the 'changed' event over 'change' this.resource.on('change', this.changed, this); this.resource.on('add', this.added, this); this.resource.on('remove', this.removed, this); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts index 06010a7fbe3..f6a2e2d3e4f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collection.ts @@ -10,7 +10,6 @@ import type { } from './helperTypes'; import { parseResourceUrl } from './resource'; import { serializeResource } from './serializers'; -import type { Collection } from './specifyTable'; import { genericTables, tables } from './tables'; import type { Tables } from './types'; @@ -24,16 +23,14 @@ export type CollectionFetchFilters = Partial< number > > & { - readonly limit?: number; - readonly reset?: boolean; + readonly limit: number; readonly offset?: number; - readonly domainFilter?: boolean; + readonly domainFilter: boolean; readonly orderBy?: | keyof CommonFields | keyof SCHEMA['fields'] | `-${string & keyof CommonFields}` | `-${string & keyof SCHEMA['fields']}`; - readonly success?: (collection: Collection) => void; }; export const DEFAULT_FETCH_LIMIT = 20; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts index e05b3312bba..f74ebf47166 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/collectionApi.ts @@ -2,13 +2,8 @@ import _ from 'underscore'; -import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; -import { DEFAULT_FETCH_LIMIT } from './collection'; -import type { AnySchema } from './helperTypes'; -import type { SpecifyResource } from './legacyTypes'; // REFACTOR: remove @ts-nocheck @@ -19,51 +14,14 @@ const Base = Backbone.Collection.extend({ }, }); -export const isRelationshipCollection = (value: unknown): boolean => - value instanceof DependentCollection || - value instanceof IndependentCollection; - function notSupported() { throw new Error('method is not supported'); } -async function fakeFetch(rawOptions) { - const options = { - ...rawOptions, - }; - if (typeof options.success === 'function') - options.success.call(options.context, this, undefined, options); +async function fakeFetch() { return this; } -async function lazyFetch(options) { - assert(this instanceof LazyCollection); - const self = this; - if (this._fetch) return this._fetch; - if (this.related?.isNew()) return fakeFetch.call(this, options); - - this._neverFetched = false; - - options ||= {}; - - options.update ??= true; - options.remove ??= false; - options.silent = true; - assert(options.at == null); - - options.data = - options.data || _.extend({ domainfilter: this.domainfilter }, this.filters); - options.data.offset = options.offset ?? this.length; - options.data.orderby = options.orderby; - - _(options).has('limit') && (options.data.limit = options.limit); - this._fetch = Backbone.Collection.prototype.fetch.call(this, options); - return this._fetch.then(() => { - self._fetch = null; - return self; - }); -} - function setupToOne(collection, options) { collection.field = options.field; collection.related = options.related; @@ -86,15 +44,20 @@ export const DependentCollection = Base.extend({ Base.call(this, records, options); }, initialize(_tables, options) { - setupToOne(this, options); this.on( 'add remove', function () { + /* + * Warning: changing a collection record does not trigger a + * change event in the parent (though it probably should) + */ this.trigger('saverequired'); }, this ); + setupToOne(this, options); + /* * If the id of the related resource changes, we go through and update * all the objects that point to it with the new pointer. @@ -117,12 +80,7 @@ export const DependentCollection = Base.extend({ isComplete() { return true; }, - getFetchOffset() { - return 0; - }, - async fetch(options) { - return fakeFetch.call(this, options); - }, + fetch: fakeFetch, sync: notSupported, create: notSupported, }); @@ -133,7 +91,6 @@ export const LazyCollection = Base.extend({ constructor(options = {}) { this.table = this.model; Base.call(this, null, options); - this._totalCount = undefined; this.filters = options.filters || {}; this.domainfilter = Boolean(options.domainfilter) && @@ -143,7 +100,7 @@ export const LazyCollection = Base.extend({ return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`; }, isComplete() { - return !this._neverFetched && this.length === this._totalCount; + return this.length === this._totalCount; }, parse(resp) { let objects; @@ -159,133 +116,44 @@ export const LazyCollection = Base.extend({ return objects; }, async fetch(options) { - if (this.isComplete()) { + this._neverFetched = false; + + if (this._fetch) return this._fetch; + else if (this.isComplete() || this.related?.isNew()) return this; + + if (this.isComplete()) console.error('fetching for already filled collection'); + + options ||= {}; + + options.update = true; + options.remove = false; + options.silent = true; + assert(options.at == null); + + options.data = + options.data || + _.extend({ domainfilter: this.domainfilter }, this.filters); + options.data.offset = this.length; + + _(options).has('limit') && (options.data.limit = options.limit); + this._fetch = Backbone.Collection.prototype.fetch.call(this, options); + return this._fetch.then(() => { + this._fetch = null; return this; - } - return lazyFetch.call(this, options); + }); }, async fetchIfNotPopulated() { return this._neverFetched && this.related?.isNew() !== true ? this.fetch() : this; }, - getFetchOffset() { - return this.length; - }, getTotalCount() { if (_.isNumber(this._totalCount)) return Promise.resolve(this._totalCount); return this.fetchIfNotPopulated().then((_this) => _this._totalCount); }, }); -export const IndependentCollection = LazyCollection.extend({ - __name__: 'IndependentCollectionBase', - constructor(options) { - this.table = this.model; - Base.call(this, null, options); - this.filters = options.filters || {}; - this.domainfilter = - Boolean(options.domainfilter) && - this.model?.specifyTable.getScopingRelationship() !== undefined; - - this._totalCount = 0; - this.removed = new Set(); - this.updated = {}; - }, - initialize(_tables, options) { - setupToOne(this, options); - - this.on( - 'change', - function (resource: SpecifyResource) { - if (!resource.isBeingInitialized()) { - if (relationshipIsToMany(this.field)) { - const otherSideName = this.field.getReverse().name; - this.related.set(otherSideName, resource); - } - this.updated[resource.cid] = resource; - this.trigger('saverequired'); - } - }, - this - ); - - this.on( - 'add', - function (resource: SpecifyResource) { - if (resource.isNew()) { - this.updated[resource.cid] = resource; - } else { - this.removed.delete(resource.url()); - this.updated[resource.cid] = resource.url(); - } - this._totalCount += 1; - this.trigger('saverequired'); - }, - this - ); - - this.on( - 'remove', - function (resource: SpecifyResource) { - if (!resource.isNew()) { - this.removed.add(resource.url()); - } - this.updated = removeKey(this.updated, resource.cid); - this._totalCount -= 1; - this.trigger('saverequired'); - }, - this - ); - - this.listenTo(this.related, 'saved', function () { - this.updated = {}; - this.removed = new Set(); - }); - }, - parse(resp) { - const self = this; - const records = Reflect.apply( - LazyCollection.prototype.parse, - this, - arguments - ); - - this._totalCount -= (this.removed as ReadonlySet).size; - - return records.filter( - ({ resource_uri }) => - !(this.removed as ReadonlySet).has(resource_uri) - ); - }, - async fetch(options) { - // If the related is being fetched, don't try and fetch the collection - if (this.related._fetch !== null) return fakeFetch.call(this, options); - - this.filters[this.field.name.toLowerCase()] = this.related.id; - - const newOptions = { - ...options, - update: options?.reset !== true, - offset: options?.offset ?? this.getFetchOffset(), - }; - - return lazyFetch.call(this, newOptions); - }, - getFetchOffset() { - return this.length === 0 && this.removed.size > 0 - ? this.removed.size - : Math.floor(this.length / DEFAULT_FETCH_LIMIT) * DEFAULT_FETCH_LIMIT; - }, - toApiJSON(options) { - return { - update: Object.values(this.updated), - remove: Array.from(this.removed), - }; - }, -}); - export const ToOneCollection = LazyCollection.extend({ __name__: 'LazyToOneCollectionBase', initialize(_models, options) { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts index 8967300d881..3a09c959b1f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/helpers.ts @@ -1,6 +1,5 @@ import { f } from '../../utils/functools'; import type { RA, ValueOf } from '../../utils/types'; -import { caseInsensitiveHash } from '../../utils/utils'; import { isTreeResource } from '../InitialContext/treeRanks'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { @@ -41,7 +40,7 @@ const weekDayMap = { Thursday: 5, Friday: 6, Saturday: 7, -} as const; +}; /** * Use this to construct a query using a lookup for Django. @@ -100,7 +99,7 @@ export const backendFilter = (field: string) => ({ [[field, 'day'].join(djangoLookupSeparator)]: value, }), monthEquals: (value: number) => ({ - [[field, 'month'].join(djangoLookupSeparator)]: value, + [[field, 'lte'].join(djangoLookupSeparator)]: value, }), yearEquals: (value: number) => ({ [[field, 'year'].join(djangoLookupSeparator)]: value, @@ -108,13 +107,8 @@ export const backendFilter = (field: string) => ({ weekEquals: (value: number) => ({ [[field, 'week'].join(djangoLookupSeparator)]: value, }), - weekDayEquals: ( - value: ValueOf | keyof typeof weekDayMap - ) => ({ - [[field, 'week_day'].join(djangoLookupSeparator)]: - typeof value === 'number' - ? value - : caseInsensitiveHash(weekDayMap, value), + weekDayEquals: (value: keyof typeof weekDayMap) => ({ + [[field, 'week_day'].join(djangoLookupSeparator)]: weekDayMap[value], }), }); diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts index 13bb62f3826..bf0d9ee2289 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/legacyTypes.ts @@ -4,7 +4,6 @@ import type { IR, RA } from '../../utils/types'; import type { BusinessRuleManager } from './businessRules'; -import type { CollectionFetchFilters } from './collection'; import type { AnySchema, CommonFields, @@ -114,8 +113,7 @@ export type SpecifyResource = { VALUE extends (SCHEMA['toManyDependent'] & SCHEMA['toManyIndependent'])[FIELD_NAME] >( - fieldName: FIELD_NAME, - filters?: CollectionFetchFilters + fieldName: FIELD_NAME ): Promise>; set< FIELD_NAME extends @@ -158,10 +156,6 @@ export type SpecifyResource = { ): SpecifyResource; // Not type safe bulkSet(value: IR): SpecifyResource; - // Unsafe - readonly independentResources: IR< - Collection | SpecifyResource | null | undefined - >; // Unsafe. Use getDependentResource instead whenever possible readonly dependentResources: IR< Collection | SpecifyResource | null | undefined diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts index a3a6f106d25..5126ca3e798 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/resourceApi.ts @@ -7,18 +7,9 @@ import { Http } from '../../utils/ajax/definitions'; import { removeKey } from '../../utils/utils'; import { assert } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import { Backbone } from './backbone'; import { attachBusinessRules } from './businessRules'; -import { CollectionFetchFilters } from './collection'; -import { isRelationshipCollection } from './collectionApi'; import { backboneFieldSeparator } from './helpers'; -import type { - AnySchema, - SerializedRecord, - SerializedResource, -} from './helperTypes'; -import type { SpecifyResource } from './legacyTypes'; import { getFieldsToNotClone, getResourceApiUrl, @@ -28,9 +19,6 @@ import { } from './resource'; import { initializeResource } from './scoping'; import { specialFields } from './serializers'; -import type { LiteralField, Relationship } from './specifyField'; -import type { Collection, SpecifyTable } from './specifyTable'; -import type { Tables } from './types'; // REFACTOR: remove @ts-nocheck @@ -41,6 +29,7 @@ function eventHandlerForToOne(related, field) { switch (event) { case 'saverequired': { this.handleChanged(); + this.trigger.apply(this, args); return; } case 'change:id': { @@ -62,7 +51,7 @@ function eventHandlerForToOne(related, field) { }; } -function eventHandlerForToMany(related, field) { +function eventHandlerForToMany(_related, field) { return function (event) { const args = _.toArray(arguments); switch (event) { @@ -72,15 +61,14 @@ function eventHandlerForToMany(related, field) { } case 'saverequired': { this.handleChanged(); + this.trigger.apply(this, args); break; } - case 'change': case 'add': case 'remove': { // Annotate add and remove events with the field in which they occurred args[0] = `${event}:${field.name.toLowerCase()}`; this.trigger.apply(this, args); - Reflect.apply(this.trigger, this, ['change', this, related]); break; } } @@ -88,15 +76,7 @@ function eventHandlerForToMany(related, field) { } // Always returns a resource -const maybeMakeResource = < - TABLE extends SpecifyTable, - TABLE_SCHEMA extends Tables[TABLE['name']] ->( - value: - | Partial | SerializedResource> - | SpecifyResource, - relatedTable: TABLE -): SpecifyResource => +const maybeMakeResource = (value, relatedTable) => value instanceof ResourceBase ? value : new relatedTable.Resource(value, { parse: true }); @@ -109,7 +89,7 @@ export const ResourceBase = Backbone.Model.extend({ _save: null, // Stores reference to the ajax deferred while the resource is being saved /** - * Returns true if the resource is being fetched or saved from Backbone + * Returns true if the resource is being fetched and saved from Backbone * More specifically, returns true while this resource holds a reference * to Backbone's save() and fetch() in _save and _fetch */ @@ -120,7 +100,6 @@ export const ResourceBase = Backbone.Model.extend({ constructor() { this.specifyTable = this.constructor.specifyTable; this.dependentResources = {}; // References to related objects referred to by field in this resource - this.independentResources = {}; Reflect.apply(Backbone.Model, this, arguments); // TEST: check if this is necessary }, initialize(attributes, options) { @@ -242,10 +221,7 @@ export const ResourceBase = Backbone.Model.extend({ // Case insensitive return Backbone.Model.prototype.get.call(this, attribute.toLowerCase()); }, - storeDependent( - field: Relationship, - related: Collection | SpecifyResource | null - ): void { + storeDependent(field, related) { assert(field.isDependent()); const setter = field.type === 'one-to-many' @@ -253,7 +229,7 @@ export const ResourceBase = Backbone.Model.extend({ : '_setDependentToOne'; this[setter](field, related); }, - _setDependentToOne(field: Relationship, related) { + _setDependentToOne(field, related) { const oldRelated = this.dependentResources[field.name.toLowerCase()]; if (!related) { if (oldRelated) { @@ -289,7 +265,7 @@ export const ResourceBase = Backbone.Model.extend({ } } }, - _setDependentToMany(field: Relationship, toMany: Collection) { + _setDependentToMany(field, toMany) { const oldToMany = this.dependentResources[field.name.toLowerCase()]; oldToMany && oldToMany.off('all', null, this); @@ -297,61 +273,6 @@ export const ResourceBase = Backbone.Model.extend({ this.dependentResources[field.name.toLowerCase()] = toMany; toMany.on('all', eventHandlerForToMany(toMany, field), this); }, - storeIndependent( - field: Relationship, - related: Collection | SpecifyResource | null - ) { - assert(!field.isDependent()); - - if (relationshipIsToMany(field)) - this._storeIndependentToMany(field, related); - else this._storeIndependentToOne(field, related); - }, - _storeIndependentToOne( - field: Relationship, - related: SpecifyResource | null - ) { - const oldRelated = this.independentResources[field.name.toLowerCase()]; - if (!related) { - if (oldRelated) { - oldRelated.off('all', null, this); - this.trigger('saverequired'); - } - this.independentResources[field.name.toLowerCase()] = null; - return; - } - - if (oldRelated && oldRelated.cid === related.cid) return; - - oldRelated && oldRelated.off('all', null, this); - - related.on('all', eventHandlerForToOne(related, field), this); - - switch (field.type) { - case 'one-to-one': - case 'many-to-one': { - this.independentResources[field.name.toLowerCase()] = related; - break; - } - case 'zero-to-one': { - this.independentResources[field.name.toLowerCase()] = related; - related.set(field.otherSideName, this.url()); - break; - } - default: { - throw new Error( - `storeIndependentToOne: unhandled field type: ${field.type} for ${this.specifyTable.name}.${field.name}` - ); - } - } - }, - _storeIndependentToMany(field: Relationship, toMany: Collection) { - const oldIndependent = this.independentResources[field.name.toLowerCase()]; - if (oldIndependent !== undefined) oldIndependent.off('all', null, this); - - this.independentResources[field.name.toLowerCase()] = toMany; - toMany.on('all', eventHandlerForToMany(toMany, field), this); - }, // Separate name to simplify typing bulkSet(attributes, options) { return this.set(attributes, options); @@ -472,7 +393,7 @@ export const ResourceBase = Backbone.Model.extend({ }, _handleInlineDataOrResource(value, fieldName) { // BUG: check type of value - const field: Relationship = this.specifyTable.strictGetField(fieldName); + const field = this.specifyTable.getField(fieldName); const relatedTable = field.relatedTable; // BUG: don't do anything for virtual fields @@ -488,23 +409,15 @@ export const ResourceBase = Backbone.Model.extend({ ); this.storeDependent(field, collection); } else { - const collection = new relatedTable.IndependentCollection( - collectionOptions, - value + console.warn( + 'got unexpected inline data for independent collection field', + { collection: this, field, value } ); - this.storeIndependent(field, collection); } // Because the foreign key is on the other side this.trigger(`change:${fieldName}`, this); this.trigger('change', this); - - /** - * These are serialized and added to the JSON before being sent to the - * server and are not in the resource's attributes - * - * https://backbonejs.org/#Sync - */ return undefined; } case 'many-to-one': { @@ -513,14 +426,13 @@ export const ResourceBase = Backbone.Model.extend({ * BUG: tighten up this check. * The FK is null, or not a URI or inlined resource at any rate */ - if (field.isDependent()) this.storeDependent(field, null); - else this.storeIndependent(field, null); + field.isDependent() && this.storeDependent(field, null); return value; } const toOne = maybeMakeResource(value, relatedTable); - if (field.isDependent()) this.storeDependent(field, toOne); - else this.storeIndependent(field, toOne); + + field.isDependent() && this.storeDependent(field, toOne); this.trigger(`change:${fieldName}`, this); this.trigger('change', this); return toOne.url(); @@ -607,12 +519,8 @@ export const ResourceBase = Backbone.Model.extend({ ); }, // Duplicate definition for purposes of better typing: - async rgetCollection(fieldName, rawOptions) { - const options = { - ...rawOptions, - prePop: true, - }; - return this.getRelated(fieldName, options); + async rgetCollection(fieldName) { + return this.getRelated(fieldName, { prePop: true }); }, async getRelated(fieldName, options) { options ||= { @@ -636,23 +544,14 @@ export const ResourceBase = Backbone.Model.extend({ if (!value) return value; // Ok if the related resource doesn't exist else if (typeof value.fetchIfNotPopulated === 'function') return value.fetchIfNotPopulated(); - /* - * Relationship Collections have already been fetched through _rget. - * This is needed to prevent refetching the collection with the default - * limit of 20 - */ else if (isRelationshipCollection(value)) return value; else if (typeof value.fetch === 'function') return value.fetch(); } return value; }); }, - async _rget( - path: RA, - options: OPTIONS - ) { + async _rget(path, options) { let fieldName = path[0].toLowerCase(); - const field: LiteralField | Relationship | undefined = - this.specifyTable.getField(fieldName); + const field = this.specifyTable.getField(fieldName); field && (fieldName = field.name.toLowerCase()); // In case fieldName is an alias let value = this.get(fieldName); field || @@ -686,21 +585,15 @@ export const ResourceBase = Backbone.Model.extend({ if (!value) return value; // No related object // Is the related resource cached? - let toOne = - this.dependentResources[fieldName] ?? - this.independentResources[fieldName]; - + let toOne = this.dependentResources[fieldName]; if (!toOne) { _(value).isString() || softFail('expected URI, got', value); toOne = resourceFromUrl(value, { noBusinessRules: options.noBusinessRules, }); - if (field.isDependent()) { console.warn('expected dependent resource to be in cache'); this.storeDependent(field, toOne); - } else { - this.storeIndependent(field, toOne); } } // If we want a field within the related resource then recur @@ -711,9 +604,41 @@ export const ResourceBase = Backbone.Model.extend({ throw "can't traverse into a collection using dot notation"; } - return field.isDependent() - ? this.getDependentToMany(field, options) - : this.getIndependentToMany(field, options); + // Is the collection cached? + let toMany = this.dependentResources[fieldName]; + if (!toMany) { + const collectionOptions = { + field: field.getReverse(), + related: this, + }; + + if (!field.isDependent()) { + return new related.ToOneCollection(collectionOptions); + } + + if (this.isNew()) { + toMany = new related.DependentCollection(collectionOptions, []); + this.storeDependent(field, toMany); + return toMany; + } else { + console.warn('expected dependent resource to be in cache'); + const temporaryCollection = new related.ToOneCollection( + collectionOptions + ); + return temporaryCollection + .fetch({ limit: 0 }) + .then( + () => + new related.DependentCollection( + collectionOptions, + temporaryCollection.tables + ) + ) + .then((toMany) => { + _this.storeDependent(field, toMany); + }); + } + } } case 'zero-to-one': { /* @@ -755,78 +680,7 @@ export const ResourceBase = Backbone.Model.extend({ } } }, - async getDependentToMany( - field: Relationship, - filters - ): Promise> { - assert(field.isDependent()); - - const self = this; - const fieldName = field.name.toLowerCase(); - const relatedTable = field.relatedTable; - - const existingToMany: Collection | undefined = - this.dependentResources[fieldName]; - - const collectionOptions = { - field: field.getReverse(), - related: this, - }; - - if (!this.isNew() && existingToMany === undefined) - console.warn('expected dependent resource to be in cache'); - - const collection = - existingToMany === undefined - ? this.isNew() - ? new relatedTable.DependentCollection(collectionOptions, []) - : await new relatedTable.ToOneCollection(collectionOptions) - .fetch({ ...filters, limit: 0 }) - .then( - (collection) => - new relatedTable.DependentCollection( - collectionOptions, - collection.models - ) - ) - : existingToMany; - - return collection.fetch({ ...filters, limit: 0 }).then((collection) => { - self.storeDependent(field, collection); - return collection; - }); - }, - async getIndependentToMany( - field: Relationship, - filters - ): Promise> { - assert(!field.isDependent()); - - const fieldName = field.name.toLowerCase(); - const relatedTable = field.relatedTable; - - const existingToMany: Collection | undefined = - this.independentResources[fieldName]; - - const collectionOptions = { - field: field.getReverse(), - related: this, - }; - - const collection = - existingToMany === undefined - ? new relatedTable.IndependentCollection(collectionOptions) - : existingToMany; - - return collection.fetch({ - ...filters, - // Only store the collection if fetch is successful (doesn't return undefined) - success: (collection) => { - this.storeIndependent(field, collection); - }, - }); - }, - async save({ + save({ onSaveConflict: handleSaveConflict, errorOnAlreadySaving = true, } = {}) { @@ -882,29 +736,16 @@ export const ResourceBase = Backbone.Model.extend({ }, toJSON() { const self = this; - const options = arguments; - const json = Backbone.Model.prototype.toJSON.apply(self, options); + const json = Backbone.Model.prototype.toJSON.apply(self, arguments); _.each(self.dependentResources, (related, fieldName) => { const field = self.specifyTable.getField(fieldName); if (field.type === 'zero-to-one') { - json[fieldName] = related ? [related.toJSON(options)] : []; + json[fieldName] = related ? [related.toJSON()] : []; } else { - json[fieldName] = related ? related.toJSON(options) : null; + json[fieldName] = related ? related.toJSON() : null; } }); - - Object.entries(self.independentResources).forEach( - ([fieldName, related]) => { - if (related) { - json[fieldName] = isRelationshipCollection(related) - ? related.toApiJSON(options) - : related.isNew() || related.needsSaved - ? related.toJSON(options) - : related.url(); - } - } - ); if (typeof this.get('resource_uri') !== 'string') json._tableName = this.specifyTable.name; return json; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx index ed6ce30ac50..117f0e085e0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx +++ b/specifyweb/frontend/js_src/lib/components/DataModel/saveBlockers.tsx @@ -9,7 +9,6 @@ import { eventListener } from '../../utils/events'; import { f } from '../../utils/functools'; import type { GetOrSet, RA } from '../../utils/types'; import { filterArray } from '../../utils/types'; -import type { SET } from '../../utils/utils'; import { removeItem } from '../../utils/utils'; import { softError } from '../Errors/assert'; import { softFail } from '../Errors/Crash'; @@ -112,7 +111,7 @@ export function useSaveBlockers( export function setSaveBlockers( resource: SpecifyResource, field: LiteralField | Relationship, - errors: Parameters>[typeof SET]>[0], + errors: Parameters>[1]>[0], blockerKey: string ): void { const resolvedErrors = @@ -201,25 +200,28 @@ const getAllBlockers = ( resources: [resource], })) ?? []), ...filterArray( - Object.entries({ - ...resource.dependentResources, - ...resource.independentResources, - }).flatMap(([fieldName, collectionOrResource]) => - (filterBlockers !== undefined && - fieldName.toLowerCase() !== filterBlockers?.name.toLowerCase()) || - collectionOrResource === undefined || - collectionOrResource === null - ? undefined - : (collectionOrResource instanceof ResourceBase - ? getAllBlockers(collectionOrResource as SpecifyResource) - : (collectionOrResource as Collection).models.flatMap( - f.unary(getAllBlockers) - ) - ).map(({ field, resources, message }) => ({ - field: [resource.specifyTable.strictGetField(fieldName), ...field], - resources: [...resources, resource], - message, - })) + Object.entries(resource.dependentResources).flatMap( + ([fieldName, collectionOrResource]) => + (filterBlockers !== undefined && + fieldName.toLowerCase() !== filterBlockers?.name.toLowerCase()) || + collectionOrResource === undefined || + collectionOrResource === null + ? undefined + : (collectionOrResource instanceof ResourceBase + ? getAllBlockers( + collectionOrResource as SpecifyResource + ) + : (collectionOrResource as Collection).models.flatMap( + f.unary(getAllBlockers) + ) + ).map(({ field, resources, message }) => ({ + field: [ + resource.specifyTable.strictGetField(fieldName), + ...field, + ], + resources: [...resources, resource], + message, + })) ) ), ]; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts index 264ca5d0c0f..046ffc44bd3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts @@ -83,14 +83,6 @@ export const schemaExtras: { indexed: false, unique: false, }), - new LiteralField(table, { - name: 'isMemberOfCOG', - required: false, - readOnly: true, - type: 'java.lang.Boolean', - indexed: false, - unique: false, - }), new LiteralField(table, { // TODO: LiteralField or Relationship? name: 'age', diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts b/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts index 304640ff3ec..20279182832 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/scoping.ts @@ -1,5 +1,6 @@ import type { RA } from '../../utils/types'; import { takeBetween } from '../../utils/utils'; +import { raise } from '../Errors/Crash'; import { getCollectionPref } from '../InitialContext/remotePrefs'; import { getTablePermissions } from '../Permissions'; import { hasTablePermission } from '../Permissions/helpers'; @@ -9,11 +10,11 @@ import type { AnySchema } from './helperTypes'; import type { SpecifyResource } from './legacyTypes'; import { getResourceApiUrl, idFromUrl } from './resource'; import { schema } from './schema'; -import { serializeResource } from './serializers'; import type { Relationship } from './specifyField'; import type { SpecifyTable } from './specifyTable'; import { strictGetTable, tables } from './tables'; -import type { CollectionObject, Tables } from './types'; +import type { CollectionObject } from './types'; +import type { Tables } from './types'; /** * Some tasks to do after a new resource is created @@ -50,26 +51,27 @@ export function initializeResource(resource: SpecifyResource): void { getCollectionPref('CO_CREATE_PREP', schema.domainLevelIds.collection) && hasTablePermission('Preparation', 'create') && resource.createdBy !== 'clone' - ) { - const preps = collectionObject.getDependentResource('preparations') ?? []; - if (preps.length === 0) - collectionObject.set('preparations', [ - serializeResource(new tables.Preparation.Resource()), - ]); - } + ) + collectionObject + .rgetCollection('preparations') + .then((preparations) => { + if (preparations.models.length === 0) + preparations.add(new tables.Preparation.Resource()); + }) + .catch(raise); if ( getCollectionPref('CO_CREATE_DET', schema.domainLevelIds.collection) && hasTablePermission('Determination', 'create') && resource.createdBy !== 'clone' - ) { - const determinations = - collectionObject.getDependentResource('determinations') ?? []; - if (determinations.length === 0) - collectionObject.set('determinations', [ - serializeResource(new tables.Determination.Resource()), - ]); - } + ) + collectionObject + .rgetCollection('determinations') + .then((determinations) => { + if (determinations.models.length === 0) + determinations.add(new tables.Determination.Resource()); + }) + .catch(raise); } export function getDomainResource< diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts index e7abde2361e..8ce8576e7a3 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/specifyTable.ts @@ -14,10 +14,8 @@ import { error } from '../Errors/assert'; import { attachmentView } from '../FormParse/webOnlyViews'; import { parentTableRelationship } from '../Forms/parentTables'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -import type { CollectionFetchFilters } from './collection'; import { DependentCollection, - IndependentCollection, LazyCollection, ToOneCollection, } from './collectionApi'; @@ -75,13 +73,13 @@ type CollectionConstructor = new ( >; readonly domainfilter?: boolean; }, - initalResources?: RA> + tables?: RA> ) => UnFetchedCollection; export type UnFetchedCollection = { - readonly fetch: ( - filter?: CollectionFetchFilters - ) => Promise>; + readonly fetch: (filter?: { + readonly limit: number; + }) => Promise>; }; export type Collection = { @@ -89,12 +87,9 @@ export type Collection = { readonly related?: SpecifyResource; readonly _totalCount?: number; readonly models: RA>; - readonly length: number; readonly table: { readonly specifyTable: SpecifyTable; }; - readonly updated?: IR | string>; - readonly removed?: ReadonlySet; readonly constructor: CollectionConstructor; /* * Shorthand method signature is used to prevent @@ -104,19 +99,12 @@ export type Collection = { /* eslint-disable @typescript-eslint/method-signature-style */ isComplete(): boolean; getTotalCount(): Promise; - getFetchOffset(): number; - toApiJSON(): { - readonly update: RA | string>; - readonly remove: RA; - }; indexOf(resource: SpecifyResource): number; // eslint-disable-next-line @typescript-eslint/naming-convention toJSON>(): RA; add(resource: RA> | SpecifyResource): void; remove(resource: SpecifyResource): void; - fetch( - filters?: CollectionFetchFilters - ): Promise>; + fetch(filter?: { readonly limit: number }): Promise>; trigger(eventName: string): void; on(eventName: string, callback: (...args: RA) => void): void; once(eventName: string, callback: (...args: RA) => void): void; @@ -189,8 +177,6 @@ export class SpecifyTable { */ public readonly DependentCollection: CollectionConstructor; - public readonly IndependentCollection: CollectionConstructor; - /** * A Backbone collection for loading a collection of items of this type as a * backwards -to-one collection of some other resource. @@ -249,11 +235,6 @@ export class SpecifyTable { model: this.Resource, }); - this.IndependentCollection = IndependentCollection.extend({ - __name__: `${this.name}IndependentCollection`, - model: this.Resource, - }); - this.ToOneCollection = ToOneCollection.extend({ __name__: `${this.name}ToOneCollection`, model: this.Resource, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index 1c91d820482..fd6c1b62bcc 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -1393,7 +1393,6 @@ export type CollectionObject = { readonly text2: string | null; readonly inventoryDate: string | null; readonly inventoryDatePrecision: number | null; - readonly isMemberOfCOG: boolean | null; readonly modifier: string | null; readonly name: string | null; readonly notifications: string | null; @@ -6522,7 +6521,6 @@ export type CollectionObjectGroup = { }; readonly toManyDependent: { readonly children: RA; - readonly cojo: RA; }; readonly toManyIndependent: RR; }; diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx deleted file mode 100644 index 0f2e9a347dd..00000000000 --- a/specifyweb/frontend/js_src/lib/components/FormCells/COJODialog.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; - -import { useBooleanState } from '../../hooks/useBooleanState'; -import { commonText } from '../../localization/common'; -import { formsText } from '../../localization/forms'; -import { DataEntry } from '../Atoms/DataEntry'; -import type { AnySchema } from '../DataModel/helperTypes'; -import type { SpecifyResource } from '../DataModel/legacyTypes'; -import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; -import { tables } from '../DataModel/tables'; -import type { - CollectionObject, - CollectionObjectGroup, -} from '../DataModel/types'; -import { ResourceView } from '../Forms/ResourceView'; -import { Dialog } from '../Molecules/Dialog'; -import { TableIcon } from '../Molecules/TableIcon'; -import { SearchDialog } from '../SearchDialog'; - -export function COJODialog({ - parentResource, - collection, -}: { - readonly parentResource: SpecifyResource | undefined; - readonly collection: Collection | undefined; -}): JSX.Element | null { - const [isOpen, handleOpen, handleClose] = useBooleanState(); - const COJOChildrenTables = [ - tables.CollectionObject, - tables.CollectionObjectGroup, - ]; - const [state, setState] = React.useState<'Add' | 'Search' | undefined>( - undefined - ); - const [resourceTable, setResourceTable] = React.useState< - | SpecifyTable - | SpecifyTable - | undefined - >(undefined); - const [newResource, setNewResource] = React.useState< - | SpecifyResource - | SpecifyResource - | undefined - >(undefined); - - React.useEffect(() => { - if (resourceTable !== undefined) { - const createdResource = new resourceTable.Resource() as - | SpecifyResource - | SpecifyResource; - setNewResource(createdResource); - } - }, [resourceTable]); - - const handleCOJOCreation = ( - selectedResource?: - | SpecifyResource - | SpecifyResource - ): void => { - if (parentResource === undefined) return; - - const resourceToUse = selectedResource ?? newResource; - - if (resourceToUse === undefined) return; - - const newCOJO = new tables.CollectionObjectGroupJoin.Resource(); - const field = - resourceToUse.specifyTable.name === 'CollectionObject' - ? 'childCo' - : 'childCog'; - - const resourceUrl = resourceToUse.url(); - const parentResourceUrl = parentResource.url(); - - newCOJO.set(field, resourceUrl as never); - newCOJO.set('parentCog', parentResourceUrl as never); - collection?.add(newCOJO); - }; - - const handleStates = (): void => { - setState(undefined); - setResourceTable(undefined); - handleClose(); - }; - - return ( - <> - - {isOpen && ( - -
- {COJOChildrenTables.map((table) => ( -
- - {table.label} - { - setState('Add'); - setResourceTable(table); - }} - /> - { - setState('Search'); - setResourceTable(table); - }} - /> -
- ))} -
-
- )} - {state === 'Add' && - newResource !== undefined && - parentResource !== undefined ? ( - } - onAdd={undefined} - onClose={(): void => { - setState(undefined); - handleClose(); - }} - onDeleted={undefined} - onSaved={(): void => { - handleCOJOCreation(); - handleStates(); - }} - onSaving={undefined} - /> - ) : undefined} - {state === 'Search' && parentResource !== undefined ? ( - } - onClose={(): void => setState(undefined)} - onSelected={(selectedResources): void => { - selectedResources.forEach((selectedResource) => { - handleCOJOCreation(selectedResource); - }); - handleStates(); - }} - /> - ) : undefined} - - ); -} diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx index 8d59431d024..e58d70ba633 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; +import type { State } from 'typesafe-reducer'; import { useId } from '../../hooks/useId'; import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'; @@ -18,8 +19,7 @@ import { backboneFieldSeparator } from '../DataModel/helpers'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import type { Relationship } from '../DataModel/specifyField'; -import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; -import type { CollectionObjectGroup } from '../DataModel/types'; +import type { SpecifyTable } from '../DataModel/specifyTable'; import { FormMeta } from '../FormMeta'; import type { FormCellDefinition, SubViewSortField } from '../FormParse/cells'; import { attachmentView } from '../FormParse/webOnlyViews'; @@ -31,10 +31,9 @@ import type { SortConfig } from '../Molecules/Sorting'; import { SortIndicator } from '../Molecules/Sorting'; import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; -import { useSearchDialog } from '../SearchDialog'; +import { SearchDialog } from '../SearchDialog'; import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -import { COJODialog } from './COJODialog'; import { FormCell } from './index'; const cellToLabel = ( @@ -74,7 +73,6 @@ export function FormTable({ onFetchMore: handleFetchMore, isCollapsed = false, preHeaderButtons, - collection, }: { readonly relationship: Relationship; readonly isDependent: boolean; @@ -91,7 +89,6 @@ export function FormTable({ readonly onFetchMore: (() => Promise) | undefined; readonly isCollapsed: boolean | undefined; readonly preHeaderButtons?: JSX.Element; - readonly collection: Collection | undefined; }): JSX.Element { const [sortConfig, setSortConfig] = React.useState< SortConfig | undefined @@ -176,6 +173,9 @@ export function FormTable({ const [isExpanded, setExpandedRecords] = React.useState< IR >({}); + const [state, setState] = React.useState< + State<'MainState'> | State<'SearchState'> + >({ type: 'MainState' }); const [flexibleColumnWidth] = userPreferences.use( 'form', 'definition', @@ -198,24 +198,13 @@ export function FormTable({ const [maxHeight] = userPreferences.use('form', 'formTable', 'maxHeight'); - const { searchDialog, showSearchDialog } = useSearchDialog({ - forceCollection: undefined, - extraFilters: undefined, - table: relationship.relatedTable, - multiple: !isToOne, - onSelected: handleAddResources, - }); - const children = collapsedViewDefinition === undefined ? ( commonText.loading() ) : resources.length === 0 ? (

{formsText.noData()}

) : ( -
+
({ maxHeight: `${maxHeight}px`, }} viewDefinition={collapsedViewDefinition} + onScroll={handleScroll} >
({ )}
- {displayViewButton && - isExpanded[resource.cid] === true && - !resource.isNew() ? ( + {displayViewButton && isExpanded[resource.cid] === true ? ( ({
); - - const isCOJO = - relationship.relatedTable.name === 'CollectionObjectGroupJoin' && - relationship.name === 'children'; - - const addButtons = isCOJO ? ( - - } - /> - ) : typeof handleAddResources === 'function' && + const addButton = + typeof handleAddResources === 'function' && mode !== 'view' && - !disableAdding ? ( - <> - {!isDependent && - hasTablePermission(relationship.relatedTable.name, 'read') ? ( - - ) : undefined} - {hasTablePermission(relationship.relatedTable.name, 'create') ? ( - { - const resource = new relationship.relatedTable.Resource(); - handleAddResources([resource]); - }} - /> - ) : undefined} - - ) : undefined; - + !disableAdding && + hasTablePermission( + relationship.relatedTable.name, + isDependent ? 'create' : 'read' + ) ? ( + { + const resource = new relationship.relatedTable.Resource(); + handleAddResources([resource]); + } + : (): void => + setState({ + type: 'SearchState', + }) + } + /> + ) : undefined; return dialog === false ? ( {preHeaderButtons} {header} - {addButtons} + {addButton} {children} - {searchDialog} + {state.type === 'SearchState' && + typeof handleAddResources === 'function' ? ( + setState({ type: 'MainState' })} + onSelected={handleAddResources} + /> + ) : undefined} ) : ( diff --git a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx index 5abf4a3e187..ecff42dcdf8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormCells/FormTableCollection.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection } from '../DataModel/collectionApi'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; @@ -13,7 +12,6 @@ export function FormTableCollection({ collection, onAdd: handleAdd, onDelete: handleDelete, - onFetchMore: handleFetch, ...props }: Omit< Parameters[0], @@ -23,16 +21,13 @@ export function FormTableCollection({ readonly onDelete: | ((resource: SpecifyResource, index: number) => void) | undefined; - readonly onFetchMore?: ( - filters?: CollectionFetchFilters - ) => Promise | undefined>; }): JSX.Element | null { const [records, setRecords] = React.useState(Array.from(collection.models)); React.useEffect( () => resourceOn( collection, - 'add remove sort sync', + 'add remove sort', () => setRecords(Array.from(collection.models)), true ), @@ -40,11 +35,9 @@ export function FormTableCollection({ ); const handleFetchMore = React.useCallback(async () => { - await (typeof handleFetch === 'function' - ? handleFetch() - : collection.fetch()); + await collection.fetch(); setRecords(Array.from(collection.models)); - }, [collection, handleFetch]); + }, [collection]); const isDependent = collection instanceof DependentCollection; const relationship = collection.field?.getReverse(); @@ -75,7 +68,6 @@ export function FormTableCollection({ }} onFetchMore={collection.isComplete() ? undefined : handleFetchMore} {...props} - collection={collection} /> ); } diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts index 9a4a6ac76e9..55d488e32af 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts @@ -17,7 +17,6 @@ import { syncers } from '../Syncer/syncers'; import type { SimpleXmlNode } from '../Syncer/xmlToJson'; import { createSimpleXmlNode } from '../Syncer/xmlToJson'; import { createXmlSpec } from '../Syncer/xmlUtils'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; /* eslint-disable @typescript-eslint/no-magic-numbers */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -369,14 +368,6 @@ const subViewSpec = ( console.error('SubView can only be used to display a relationship'); return undefined; } - if (field !== undefined && field.getReverse() === undefined) { - console.error( - `No reverse relationship exists${ - relationshipIsToMany(field) ? '' : '. Use a querycbx instead' - }` - ); - return undefined; - } if (field?.type === 'many-to-many') { // ResourceApi does not support .rget() on a many-to-many console.warn('Many-to-many relationships are not supported'); @@ -808,32 +799,8 @@ const textAreaSpec = ( ), }); -const queryComboBoxSpec = ( - _spec: SpecToJson>, - { - table, - }: { - readonly table: SpecifyTable | undefined; - } -) => +const queryComboBoxSpec = f.store(() => createXmlSpec({ - field: pipe( - rawFieldSpec(table).field, - syncer( - ({ parsed, ...rest }) => { - if ( - parsed?.some( - (field) => field.isRelationship && relationshipIsToMany(field) - ) - ) - console.error( - 'Unable to render a to-many relationship as a querycbx. Use a Subview instead' - ); - return { parsed, ...rest }; - }, - (value) => value - ) - ), // Customize view name dialogViewName: syncers.xmlAttribute('initialize displayDlg', 'skip'), searchDialogViewName: syncers.xmlAttribute('initialize searchDlg', 'skip'), @@ -869,7 +836,8 @@ const queryComboBoxSpec = ( syncers.maybe(syncers.toBoolean), syncers.default(true) ), - }); + }) +); const checkBoxSpec = f.store(() => createXmlSpec({ diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts index cc723f79eda..1f06902f7f8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts @@ -24,7 +24,6 @@ import { getBooleanAttribute, getParsedAttribute, } from '../Syncer/xmlUtils'; -import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { PluginDefinition } from './plugins'; import { parseUiPlugin } from './plugins'; @@ -223,15 +222,6 @@ const processFieldType: { if (fields === undefined) { console.error('Trying to render a query combobox without a field name'); return { type: 'Blank' }; - } else if ( - fields.some( - (field) => field.isRelationship && relationshipIsToMany(field) - ) - ) { - console.error( - 'Unable to render a to-many relationship as a querycbx. Use a Subview instead' - ); - return { type: 'Blank' }; } else if (fields.at(-1)?.isRelationship === true) { return { type: 'QueryComboBox', diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx index fe31b2fe546..8e594d2a0a6 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import type { State } from 'typesafe-reducer'; import { useSearchParameter } from '../../hooks/navigation'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -9,7 +8,6 @@ import type { RA } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { DataEntry } from '../Atoms/DataEntry'; import { ReadOnlyContext } from '../Core/Contexts'; -import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection } from '../DataModel/collectionApi'; import type { AnyInteractionPreparation, @@ -18,8 +16,6 @@ import type { import type { SpecifyResource } from '../DataModel/legacyTypes'; import { useAllSaveBlockers } from '../DataModel/saveBlockers'; import type { Collection, SpecifyTable } from '../DataModel/specifyTable'; -import type { CollectionObjectGroup } from '../DataModel/types'; -import { COJODialog } from '../FormCells/COJODialog'; import { FormTableCollection } from '../FormCells/FormTableCollection'; import type { FormType } from '../FormParse'; import type { SubViewSortField } from '../FormParse/cells'; @@ -46,7 +42,6 @@ export function IntegratedRecordSelector({ onClose: handleClose, onAdd: handleAdd, onDelete: handleDelete, - onFetch: handleFetch, isCollapsed: defaultCollapsed, ...rest }: Omit< @@ -58,9 +53,6 @@ export function IntegratedRecordSelector({ readonly viewName?: string; readonly urlParameter?: string; readonly onClose: () => void; - readonly onFetch?: ( - filters?: CollectionFetchFilters - ) => Promise | undefined>; readonly sortField: SubViewSortField | undefined; }): JSX.Element { const containerRef = React.useRef(null); @@ -79,19 +71,6 @@ export function IntegratedRecordSelector({ const [isCollapsed, _handleCollapsed, handleExpand, handleToggle] = useBooleanState(defaultCollapsed); - const [state, setState] = React.useState< - | State< - 'AddResourceState', - { - readonly resource: SpecifyResource; - readonly handleAdd: ( - resources: RA> - ) => void; - } - > - | State<'MainState'> - >({ type: 'MainState' }); - const blockers = useAllSaveBlockers(collection.related, relationship); const hasBlockers = blockers.length > 0; React.useEffect(() => { @@ -135,17 +114,13 @@ export function IntegratedRecordSelector({ const isAttachmentTable = collection.table.specifyTable.name.includes('Attachment'); - const isCOJO = - relationship.relatedTable.name === 'CollectionObjectGroupJoin' && - relationship.name === 'children'; - return ( { + onAdd={(resources) => { if (isInteraction) { setInteractionResource(resources[0]); handleOpenDialog(); @@ -158,7 +133,6 @@ export function IntegratedRecordSelector({ if (isCollapsed) handleExpand(); handleDelete?.(...args); }} - onFetch={handleFetch} onSlide={(index): void => { handleExpand(); if (typeof urlParameter === 'string') setIndex(index.toString()); @@ -171,7 +145,6 @@ export function IntegratedRecordSelector({ resource, onAdd: handleAdd, onRemove: handleRemove, - showSearchDialog, isLoading, }): JSX.Element => ( <> @@ -205,63 +178,23 @@ export function IntegratedRecordSelector({ !isDependent && dialog === false ? resource : undefined } /> - {!isDependent && - hasTablePermission( + {hasTablePermission( relationship.relatedTable.name, - 'read' - ) && - typeof handleAdd === 'function' ? ( - 0) } - onClick={showSearchDialog} + onClick={(): void => { + focusFirstField(); + const resource = + new collection.table.specifyTable.Resource(); + handleAdd([resource]); + }} /> ) : undefined} - {hasTablePermission( - relationship.relatedTable.name, - 'create' - ) && typeof handleAdd === 'function' ? ( - isCOJO ? ( - - } - /> - ) : ( - 0) - } - onClick={(): void => { - const resource = - new collection.table.specifyTable.Resource(); - - if ( - isDependent || - viewName === relationship.relatedTable.view - ) { - focusFirstField(); - handleAdd([resource]); - return; - } - - if (state.type === 'AddResourceState') - setState({ type: 'MainState' }); - else - setState({ - type: 'AddResourceState', - resource, - handleAdd, - }); - }} - /> - ) - ) : undefined} {hasTablePermission( relationship.relatedTable.name, isDependent ? 'delete' : 'read' @@ -328,26 +261,9 @@ export function IntegratedRecordSelector({ if (isCollapsed) handleExpand(); handleDelete?.(index, 'minusButton'); }} - onFetchMore={handleFetch} /> ) : null} {dialogs} - {state.type === 'AddResourceState' && - typeof handleAdd === 'function' ? ( - setState({ type: 'MainState' })} - onDeleted={undefined} - onSaved={(): void => { - state.handleAdd([state.resource]); - setState({ type: 'MainState' }); - }} - /> - ) : null} )} diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx index c84897a17df..08e845d3e23 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelector.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type { State } from 'typesafe-reducer'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; @@ -7,8 +8,7 @@ import type { AnySchema } from '../DataModel/helperTypes'; 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 { SearchDialog } from '../SearchDialog'; import { Slider } from './Slider'; export type RecordSelectorProps = { @@ -56,7 +56,6 @@ export type RecordSelectorState = { readonly onRemove: | ((source: 'deleteButton' | 'minusButton') => void) | undefined; - readonly showSearchDialog: () => void; // True while fetching new record readonly isLoading: boolean; }; @@ -83,33 +82,9 @@ export function useRecordSelector({ [index] ); - const isToOne = !relationshipIsToMany(field) || field?.type === 'zero-to-one'; - - const handleResourcesSelected = React.useMemo( - () => - typeof handleAdded === 'function' - ? (resources: RA>): void => { - if (field?.isDependent() ?? true) - f.maybe(field?.otherSideName, (fieldName) => - f.maybe(relatedResource?.url(), (url) => - resources.forEach((resource) => { - resource.set(fieldName, url as never); - }) - ) - ); - handleAdded(resources); - } - : undefined, - [handleAdded, relatedResource, field] - ); - - const { searchDialog, showSearchDialog } = useSearchDialog({ - extraFilters: undefined, - forceCollection: undefined, - multiple: !isToOne, - table, - onSelected: handleResourcesSelected, - }); + const [state, setState] = React.useState< + State<'AddBySearch'> | State<'Main'> + >({ type: 'Main' }); return { slider: ( @@ -119,7 +94,7 @@ export function useRecordSelector({ onChange={ handleSlide === undefined ? undefined - : (index): void => handleSlide?.(index, false) + : (index) => handleSlide?.(index, false) } /> ), @@ -128,7 +103,26 @@ export function useRecordSelector({ isLoading: records[index] === undefined && totalCount !== 0, // While new resource is loading, display previous resource resource: records[index] ?? records[lastIndexRef.current], - dialogs: searchDialog, + dialogs: + state.type === 'AddBySearch' && typeof handleAdded === 'function' ? ( + setState({ type: 'Main' })} + onSelected={(resources): void => { + f.maybe(field?.otherSideName, (fieldName) => + f.maybe(relatedResource?.url(), (url) => + resources.forEach((resource) => + resource.set(fieldName, url as never) + ) + ) + ); + handleAdded(resources); + }} + /> + ) : null, onAdd: typeof handleAdded === 'function' ? (resources: RA>): void => { @@ -140,7 +134,7 @@ export function useRecordSelector({ ) resource.set(field.otherSideName, relatedResource.url() as any); handleAdded([resource]); - } else showSearchDialog(); + } else setState({ type: 'AddBySearch' }); } : undefined, onRemove: @@ -162,6 +156,5 @@ export function useRecordSelector({ ) : undefined : undefined, - showSearchDialog, }; } diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx index 05fcc9b6acd..76485597dfd 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSelectorFromCollection.tsx @@ -3,10 +3,8 @@ import React from 'react'; import { useTriggerState } from '../../hooks/useTriggerState'; import type { RA } from '../../utils/types'; import { defined } from '../../utils/types'; -import type { CollectionFetchFilters } from '../DataModel/collection'; import { DependentCollection, - isRelationshipCollection, LazyCollection, } from '../DataModel/collectionApi'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -14,6 +12,7 @@ import type { SpecifyResource } from '../DataModel/legacyTypes'; import { resourceOn } from '../DataModel/resource'; import type { Relationship } from '../DataModel/specifyField'; import type { Collection } from '../DataModel/specifyTable'; +import { raise } from '../Errors/Crash'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; import type { RecordSelectorProps, @@ -27,7 +26,6 @@ export function RecordSelectorFromCollection({ onAdd: handleAdd, onDelete: handleDelete, onSlide: handleSlide, - onFetch: handleFetch, children, defaultIndex = 0, ...rest @@ -46,7 +44,6 @@ export function RecordSelectorFromCollection({ readonly relationship: Relationship; readonly defaultIndex?: number; readonly children: (state: RecordSelectorState) => JSX.Element; - readonly onFetch?: (filters?: CollectionFetchFilters) => void; }): JSX.Element | null { const getRecords = React.useCallback( (): RA | undefined> => @@ -66,7 +63,7 @@ export function RecordSelectorFromCollection({ () => resourceOn( collection, - 'add remove destroy sync', + 'add remove destroy', (): void => setRecords(getRecords), true ), @@ -82,26 +79,23 @@ export function RecordSelectorFromCollection({ * don't need to fetch all records in between) */ if ( - typeof handleFetch === 'function' && - !isToOne && isLazy && collection.related?.isNew() !== true && + !collection.isComplete() && collection.models[index] === undefined ) - handleFetch({ - offset: collection.getFetchOffset(), - }); - }, [collection, isLazy, index, records.length, isToOne, handleFetch]); + collection + .fetch() + .then(() => setRecords(getRecords)) + .catch(raise); + }, [collection, isLazy, getRecords, index, records.length]); const state = useRecordSelector({ ...rest, index, table: collection.table.specifyTable, - field: relationship, records, - relatedResource: isRelationshipCollection(collection) - ? collection.related - : undefined, + relatedResource: isDependent ? collection.related : undefined, totalCount: collection._totalCount ?? records.length, onAdd: (rawResources): void => { const resources = isToOne ? rawResources.slice(0, 1) : rawResources; diff --git a/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx b/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx index f6706b4e45f..3d0a7c8bf4f 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/DeleteButton.tsx @@ -17,7 +17,6 @@ import { icons } from '../Atoms/Icons'; import { LoadingContext } from '../Core/Contexts'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; -import { resourceOn } from '../DataModel/resource'; import { serializeResource } from '../DataModel/serializers'; import type { Relationship } from '../DataModel/specifyField'; import { strictGetTable } from '../DataModel/tables'; @@ -72,19 +71,6 @@ export function DeleteButton({ false ); - React.useEffect( - () => - deferred - ? undefined - : resourceOn( - resource, - 'saved', - () => void fetchBlockers(resource).then(setBlockers), - false - ), - [resource, deferred] - ); - const [isOpen, handleOpen, handleClose] = useBooleanState(); const loading = React.useContext(LoadingContext); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx index 67de74bc85c..7fee9fe0d06 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx @@ -2,46 +2,38 @@ import React from 'react'; import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { useCollection } from '../../hooks/useCollection'; import { useTriggerState } from '../../hooks/useTriggerState'; import { commonText } from '../../localization/common'; -import type { RA } from '../../utils/types'; +import { overwriteReadOnly } from '../../utils/types'; +import { sortFunction } from '../../utils/utils'; import { Button } from '../Atoms/Button'; -import { DataEntry } from '../Atoms/DataEntry'; import { attachmentSettingsPromise } from '../Attachments/attachments'; import { attachmentRelatedTables } from '../Attachments/utils'; import { ReadOnlyContext } from '../Core/Contexts'; -import type { CollectionFetchFilters } from '../DataModel/collection'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import { resourceOn } from '../DataModel/resource'; import type { Relationship } from '../DataModel/specifyField'; +import type { Collection } from '../DataModel/specifyTable'; +import { raise, softFail } from '../Errors/Crash'; import type { FormType } from '../FormParse'; import type { SubViewSortField } from '../FormParse/cells'; import { IntegratedRecordSelector } from '../FormSliders/IntegratedRecordSelector'; import { TableIcon } from '../Molecules/TableIcon'; import { relationshipIsToMany } from '../WbPlanView/mappingHelpers'; -type SubViewContextType = +export const SubViewContext = React.createContext< | { readonly relationship: Relationship | undefined; readonly formType: FormType; readonly sortField: SubViewSortField | undefined; - /** - * Don't render a relationship if it is already being rendered in a - * parent subview. - * Avoids infinite cycles in rendering forms - */ - readonly parentContext: RA | undefined; readonly handleChangeFormType: (formType: FormType) => void; readonly handleChangeSortField: ( sortField: SubViewSortField | undefined ) => void; } - | undefined; - -export const SubViewContext = - React.createContext(undefined); + | undefined +>(undefined); SubViewContext.displayName = 'SubViewContext'; export function SubView({ @@ -67,57 +59,135 @@ export function SubView({ }): JSX.Element { const [sortField, setSortField] = useTriggerState(initialSortField); - const [collection, _setCollection, handleFetch] = useCollection({ - parentResource, - relationship, - sortBy: sortField, - }); + const fetchCollection = React.useCallback( + async function fetchCollection(): Promise< + Collection | undefined + > { + if ( + relationshipIsToMany(relationship) && + relationship.type !== 'zero-to-one' + ) + return parentResource + .rgetCollection(relationship.name) + .then((collection) => { + // TEST: check if this can ever happen + if (collection === null || collection === undefined) + return new relationship.relatedTable.DependentCollection({ + related: parentResource, + field: relationship.getReverse(), + }) as Collection; + if (sortField === undefined) return collection; + // BUG: this does not look into related tables + const field = sortField.fieldNames[0]; + // Overwriting the tables on the collection + overwriteReadOnly( + collection, + 'models', + Array.from(collection.models).sort( + sortFunction( + (resource) => resource.get(field), + sortField.direction === 'desc' + ) + ) + ); + return collection; + }); + else { + /** + * If relationship is -to-one, create a collection for the related + * resource. This allows to reuse most of the code from the -to-many + * relationships. RecordSelector handles collections with -to-one + * related field by removing the "+" button after first record is added + * and not rendering record count or record slider. + */ + const resource = await parentResource.rgetPromise(relationship.name); + const reverse = relationship.getReverse(); + if (reverse === undefined) { + softFail( + new Error( + `Can't render a SubView for ` + + `${relationship.table.name}.${relationship.name} because ` + + `reverse relationship does not exist` + ) + ); + return undefined; + } + const collection = ( + relationship.isDependent() + ? new relationship.relatedTable.DependentCollection({ + related: parentResource, + field: reverse, + }) + : new relationship.relatedTable.LazyCollection({ + filters: { + [reverse.name]: parentResource.id, + }, + }) + ) as Collection; + if (relationship.isDependent() && parentResource.isNew()) + // Prevent fetching related for newly created parent + overwriteReadOnly(collection, '_totalCount', 0); + + if (typeof resource === 'object' && resource !== null) + collection.add(resource); + overwriteReadOnly( + collection, + 'related', + collection.related ?? parentResource + ); + overwriteReadOnly( + collection, + 'field', + collection.field ?? relationship.getReverse() + ); + return collection; + } + }, + [parentResource, relationship, sortField] + ); + const [collection, setCollection] = React.useState< + Collection | undefined + >(undefined); + const versionRef = React.useRef(0); React.useEffect( () => resourceOn( parentResource, - 'saved', + `change:${relationship.name}`, (): void => { - handleFetch({ - offset: 0, - reset: true, - } as CollectionFetchFilters); + versionRef.current += 1; + const localVersionRef = versionRef.current; + fetchCollection() + .then((collection) => + /* + * If value changed since begun fetching, don't update the + * collection to prevent a race condition. + * REFACTOR: simplify this + */ + versionRef.current === localVersionRef + ? setCollection(collection) + : undefined + ) + .catch(raise); }, - false + true ), - [parentResource, relationship, handleFetch] + [parentResource, relationship, fetchCollection] ); - const subviewContext = React.useContext(SubViewContext); - const [formType, setFormType] = useTriggerState(initialFormType); - const parentContext = React.useMemo( - () => subviewContext?.parentContext ?? [], - [subviewContext?.parentContext] - ); - - const contextValue = React.useMemo( + const contextValue = React.useMemo( () => ({ relationship, formType, sortField, - parentContext: [...parentContext, relationship], handleChangeFormType: setFormType, handleChangeSortField: setSortField, }), - [ - relationship, - formType, - sortField, - parentContext, - setFormType, - setSortField, - ] + [relationship, formType, sortField, setFormType, setSortField] ); - const isReadOnly = React.useContext(ReadOnlyContext); - const [isOpen, _, handleClose, handleToggle] = useBooleanState(!isButton); const [isAttachmentConfigured] = usePromise(attachmentSettingsPromise, true); @@ -129,16 +199,14 @@ export function SubView({ const isAttachmentMisconfigured = isAttachmentTable && !isAttachmentConfigured; + const isReadOnly = React.useContext(ReadOnlyContext); return ( - {parentContext.includes(relationship) || - collection === false ? undefined : ( - <> - {isButton && ( - 0 @@ -146,77 +214,62 @@ export function SubView({ : '' } ${isOpen ? '!bg-brand-300 dark:!bg-brand-500' : ''}`} - title={relationship.label} - onClick={handleToggle} - > - { - /* - * Attachment table icons have lots of vertical white space, making - * them look overly small on the forms. - * See https://github.com/specify/specify7/issues/1259 - * Thus, have to introduce some inconsistency here - */ - parentFormType === 'form' && ( - - ) - } - - {collection?.models.length ?? commonText.loading()} - - - )} - {typeof collection === 'object' && isOpen ? ( - - - void parentResource.set( - relationship.name, - resource as never - ) - } - onClose={handleClose} - onDelete={ - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ? undefined - : (): void => - void parentResource.set( - relationship.name, - null as never - ) - } - onFetch={handleFetch} - /> - - ) : isButton ? undefined : ( - - - - {relationship.label} - - - {commonText.loading()} - - )} - + title={relationship.label} + onClick={handleToggle} + > + { + /* + * Attachment table icons have lots of vertical white space, making + * them look overly small on the forms. + * See https://github.com/specify/specify7/issues/1259 + * Thus, have to introduce some inconsistency here + */ + parentFormType === 'form' && ( + + ) + } + + {collection?.models.length ?? commonText.loading()} + + )} + {typeof collection === 'object' && isOpen ? ( + + + void parentResource.set( + relationship.name, + resource as never + ) + } + onClose={handleClose} + onDelete={ + relationshipIsToMany(relationship) && + relationship.type !== 'zero-to-one' + ? undefined + : (): void => + void parentResource.set(relationship.name, null as never) + } + /> + + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts index 1775ae0cb61..0d778d27776 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/treeRanks.test.ts @@ -69,11 +69,11 @@ test('getTreeScope', () => expect( Object.fromEntries(testingTrees.map((tree) => [tree, getTreeScope(tree)])) ).toMatchInlineSnapshot(` - { - "Geography": "discipline", - "GeologicTimePeriod": "discipline", - "LithoStrat": "discipline", - "Storage": "institution", - "Taxon": "discipline", - } - `)); + { + "Geography": "discipline", + "GeologicTimePeriod": "discipline", + "LithoStrat": "discipline", + "Storage": "institution", + "Taxon": "discipline", + } + `)); diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx index db97a422527..8d8f7969c1d 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx @@ -536,7 +536,7 @@ export function QueryComboBox({ } /> )} - {hasViewButton && hasTablePermission(relatedTable.name, 'read') + {hasViewButton && hasTablePermission(relatedTable.name, 'create') ? viewButton : undefined} diff --git a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx index bbb48929f9e..96da3cb751f 100644 --- a/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; -import type { State } from 'typesafe-reducer'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; @@ -50,7 +49,10 @@ const viewNameExceptions: Partial> = { GeologicTimePeriod: 'ChronosStratSearch', }; -type SearchDialogProps = { +/** + * Display a resource search dialog + */ +export function SearchDialog(props: { readonly forceCollection: number | undefined; readonly extraFilters: RA> | undefined; readonly table: SpecifyTable; @@ -59,14 +61,7 @@ type SearchDialogProps = { readonly searchView?: string; readonly onSelected: (resources: RA>) => void; readonly onlyUseQueryBuilder?: boolean; -}; - -/** - * Display a resource search dialog - */ -export function SearchDialog( - props: SearchDialogProps -): JSX.Element | null { +}): JSX.Element | null { const [alwaysUseQueryBuilder] = userPreferences.use( 'form', 'queryComboBox', @@ -89,41 +84,6 @@ export function SearchDialog( ); } -/** - * Displays a SearchDialog whenever `showSearchDialog` is invoked - */ -export function useSearchDialog({ - onSelected: handleSelected, - onClose: handleClosed, - ...rest -}: Omit, 'onClose' | 'onSelected'> & - Partial, 'onClose' | 'onSelected'>>): { - readonly searchDialog: JSX.Element | null; - readonly showSearchDialog: () => void; -} { - const [state, setState] = React.useState | State<'Search'>>({ - type: 'Main', - }); - - return { - searchDialog: - state.type === 'Search' && typeof handleSelected === 'function' ? ( - { - handleClosed?.(); - setState({ type: 'Main' }); - }} - onSelected={handleSelected} - /> - ) : null, - showSearchDialog: () => - typeof handleSelected === 'function' - ? setState({ type: 'Search' }) - : undefined, - }; -} - const filterResults = ( results: RA>, extraFilters: RA> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index 04478d71858..6272bb7090b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -6,7 +6,6 @@ import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { type RA } from '../../utils/types'; import { LoadingContext } from '../Core/Contexts'; -import { backendFilter } from '../DataModel/helpers'; import type { AnySchema } from '../DataModel/helperTypes'; import type { Collection } from '../DataModel/specifyTable'; import { strictGetTable } from '../DataModel/tables'; @@ -77,7 +76,7 @@ export function useDisambiguationDialog({ ); const table = strictGetTable(tableName); const resources = new table.LazyCollection({ - filters: backendFilter('id').isIn(matches.ids), + filters: { id__in: matches.ids.join(',') }, }) as Collection; loading( diff --git a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx index b9b95b00772..1e492a3f446 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useCollection.tsx @@ -1,166 +1,89 @@ import React from 'react'; -import type { CollectionFetchFilters } from '../components/DataModel/collection'; +import type { SerializedCollection } from '../components/DataModel/collection'; import type { AnySchema } from '../components/DataModel/helperTypes'; -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 { f } from '../utils/functools'; import type { GetOrSet } from '../utils/types'; -import { overwriteReadOnly } from '../utils/types'; -import { sortFunction } from '../utils/utils'; +import { defined } from '../utils/types'; import { useAsyncState } from './useAsyncState'; -type UseCollectionProps = { - readonly parentResource: SpecifyResource; - readonly relationship: Relationship; - readonly sortBy?: SubViewSortField; - readonly filters?: CollectionFetchFilters; -}; - -export function useCollection({ - parentResource, - relationship, - sortBy, -}: UseCollectionProps): readonly [ - ...GetOrSet | false | undefined>, - ( - filters?: CollectionFetchFilters - ) => Promise | undefined> +/** + * A hook for fetching a collection of resources in a paginated way + */ +export function useCollection( + fetch: (offset: number) => Promise> +): readonly [ + SerializedCollection | undefined, + GetOrSet | undefined>[1], + () => Promise ] { - const [collection, setCollection] = useAsyncState< - Collection | false | undefined - >( - React.useCallback( - async () => - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ? fetchToManyCollection({ - parentResource, - relationship, - sortBy, - }) - : fetchToOneCollection({ - parentResource, - relationship, - }), - [sortBy, parentResource, relationship] - ), - false - ); - - const versionRef = React.useRef(0); + const fetchRef = React.useRef< + Promise | undefined> | undefined + >(undefined); - const handleFetch = React.useCallback( - async ( - filters?: CollectionFetchFilters - ): Promise | undefined> => { - if (typeof collection !== 'object') return undefined; + const callback = React.useCallback(async () => { + if (typeof fetchRef.current === 'object') + return fetchRef.current.then(f.undefined); + if ( + collectionRef.current !== undefined && + collectionRef.current?.records.length === + collectionRef.current?.totalCount + ) + return undefined; + fetchRef.current = fetch(collectionRef.current?.records.length ?? 0).then( + (data) => { + fetchRef.current = undefined; + return data; + } + ); + return fetchRef.current; + }, [fetch]); - versionRef.current += 1; - const localVersionRef = versionRef.current; + const currentCallback = React.useRef(f.void); - const fetchCollection = - relationshipIsToMany(relationship) && - relationship.type !== 'zero-to-one' - ? fetchToManyCollection({ - parentResource, - relationship, - sortBy, - filters, - }) - : fetchToOneCollection({ parentResource, relationship }); - - return fetchCollection.then((collection) => { - if ( - typeof collection === 'object' && - versionRef.current === localVersionRef - ) { - setCollection(collection); - } - return collection === false ? undefined : collection; - }); - }, - [collection, parentResource, relationship, setCollection, sortBy] + const [collection, setCollection] = useAsyncState( + React.useCallback(async () => { + currentCallback.current = callback; + fetchRef.current = undefined; + collectionRef.current = undefined; + return callback(); + }, [callback]), + false ); - return [collection, setCollection, handleFetch]; -} + const collectionRef = React.useRef< + SerializedCollection | undefined + >(); + collectionRef.current = collection; -const fetchToManyCollection = async ({ - parentResource, - relationship, - sortBy, - filters, -}: UseCollectionProps): Promise | undefined> => - parentResource - .rgetCollection(relationship.name, filters) - .then((collection) => { - // TEST: check if this can ever happen - if (collection === null || collection === undefined) - return new relationship.relatedTable.DependentCollection({ - related: parentResource, - field: relationship.getReverse(), - }) as Collection; - if (sortBy === undefined) return collection; - - // BUG: this does not look into related tables - const field = sortBy.fieldNames[0]; - - // Overwriting the models on the collection - overwriteReadOnly( - collection, - 'models', - Array.from(collection.models).sort( - sortFunction( - (resource) => resource.get(field), - sortBy.direction === 'desc' - ) - ) - ); - return collection; - }); - -async function fetchToOneCollection({ - parentResource, - relationship, -}: UseCollectionProps): Promise< - Collection | false | undefined -> { - /** - * If relationship is -to-one, create a collection for the related - * resource. This allows to reuse most of the code from -to-many - * relationships in components like Subview and RecordSelectorFromCollection - */ - const resource = await parentResource.rgetPromise(relationship.name); - const reverse = relationship.getReverse(); - if (reverse === undefined) return false; - const collection = ( - relationship.isDependent() - ? new relationship.relatedTable.DependentCollection({ - related: parentResource, - field: reverse, - }) - : new relationship.relatedTable.IndependentCollection({ - related: parentResource, - field: reverse, - }) - ) as Collection; - if (relationship.isDependent() && parentResource.isNew()) - // Prevent fetching related for newly created parent - overwriteReadOnly(collection, '_totalCount', 0); - - if (typeof resource === 'object' && resource !== null) - collection.add(resource); - overwriteReadOnly( - collection, - 'related', - collection.related ?? parentResource - ); - overwriteReadOnly( - collection, - 'field', - collection.field ?? relationship.getReverse() + const fetchMore = React.useCallback( + async () => + /* + * Ignore calls to fetchMore before collection is fetched for the first + * time + */ + currentCallback.current === callback + ? typeof fetchRef.current === 'object' + ? callback().then(f.undefined) + : callback().then((result) => + result !== undefined && + result.records.length > 0 && + // If the fetch function changed while fetching, discard the results + currentCallback.current === callback + ? setCollection((collection) => ({ + records: [ + ...defined( + collection, + 'Try to fetch more before collection is fetch.' + ).records, + ...result.records, + ], + totalCount: defined(collection).totalCount, + })) + : undefined + ) + : undefined, + [callback, collection] ); - return collection; + + return [collection, setCollection, fetchMore] as const; } diff --git a/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx b/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx deleted file mode 100644 index 1cc18d7c584..00000000000 --- a/specifyweb/frontend/js_src/lib/hooks/useSerializedCollection.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; - -import type { SerializedCollection } from '../components/DataModel/collection'; -import type { AnySchema } from '../components/DataModel/helperTypes'; -import { f } from '../utils/functools'; -import type { GetOrSet } from '../utils/types'; -import { defined } from '../utils/types'; -import { useAsyncState } from './useAsyncState'; - -/** - * A hook for fetching a collection of resources in a paginated way - */ -export function useSerializedCollection( - fetch: (offset: number) => Promise> -): readonly [ - SerializedCollection | undefined, - GetOrSet | undefined>[1], - () => Promise -] { - const fetchRef = React.useRef< - Promise | undefined> | undefined - >(undefined); - - const callback = React.useCallback(async () => { - if (typeof fetchRef.current === 'object') - return fetchRef.current.then(f.undefined); - if ( - collectionRef.current !== undefined && - collectionRef.current?.records.length === - collectionRef.current?.totalCount - ) - return undefined; - fetchRef.current = fetch(collectionRef.current?.records.length ?? 0).then( - (data) => { - fetchRef.current = undefined; - return data; - } - ); - return fetchRef.current; - }, [fetch]); - - const currentCallback = React.useRef(f.void); - - const [collection, setCollection] = useAsyncState( - React.useCallback(async () => { - currentCallback.current = callback; - fetchRef.current = undefined; - collectionRef.current = undefined; - return callback(); - }, [callback]), - false - ); - const collectionRef = React.useRef< - SerializedCollection | undefined - >(); - collectionRef.current = collection; - - const fetchMore = React.useCallback( - async () => - /* - * Ignore calls to fetchMore before collection is fetched for the first - * time - */ - currentCallback.current === callback - ? typeof fetchRef.current === 'object' - ? callback().then(f.undefined) - : callback().then((result) => - result !== undefined && - result.records.length > 0 && - // If the fetch function changed while fetching, discard the results - currentCallback.current === callback - ? setCollection((collection) => ({ - records: [ - ...defined( - collection, - 'Try to fetch more before collection is fetch.' - ).records, - ...result.records, - ], - totalCount: defined(collection).totalCount, - })) - : undefined - ) - : undefined, - [callback, collection] - ); - - return [collection, setCollection, fetchMore] as const; -} diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index b9382efdf03..9134ba47354 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1159,7 +1159,4 @@ export const formsText = createDictionary({ 'ru-ru': 'Номер по каталогу Числовой', 'uk-ua': 'Каталожний номер Числовий', }, - addCOGChildren: { - 'en-us': 'Add COG Children', - }, } as const); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json index 4a255c74ace..52fcc38131d 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/api/specify_trees.json @@ -538,6 +538,7 @@ "version": 8, "createdbyagent": null, "modifiedbyagent": "/api/specify/agent/1514/", + "institutions": "/api/specify/institution/?storagetreedef=1", "treeentries": "/api/specify/storage/?definition=1", "treedefitems": [ { @@ -735,6 +736,7 @@ "version": 3, "createdbyagent": null, "modifiedbyagent": "/api/specify/agent/1514/", + "disciplines": "/api/specify/discipline/?geographytreedef=1", "treeentries": "/api/specify/geography/?definition=1", "treedefitems": [ { diff --git a/specifyweb/permissions/permissions.py b/specifyweb/permissions/permissions.py index 5f056915536..e329ae7d608 100644 --- a/specifyweb/permissions/permissions.py +++ b/specifyweb/permissions/permissions.py @@ -5,6 +5,7 @@ from django.db import connection from django.db.models import Model +from django.core.exceptions import ObjectDoesNotExist from specifyweb.specify.models import Agent from specifyweb.specify.datamodel import Table diff --git a/specifyweb/specify/api.py b/specifyweb/specify/api.py index bf2ba273568..a315700eca4 100644 --- a/specifyweb/specify/api.py +++ b/specifyweb/specify/api.py @@ -6,7 +6,7 @@ import logging import re from typing import Any, Dict, List, Optional, Tuple, Iterable, Union, \ - Callable, TypedDict + Callable from urllib.parse import urlencode from typing_extensions import TypedDict @@ -17,7 +17,7 @@ from django.db import transaction from django.apps import apps from django.http import (HttpResponse, HttpResponseBadRequest, - Http404, HttpResponseNotAllowed, QueryDict) + Http404, HttpResponseNotAllowed, JsonResponse, QueryDict) from django.core.exceptions import ObjectDoesNotExist, FieldError, FieldDoesNotExist from django.db.models.fields import DateTimeField, FloatField, DecimalField @@ -29,7 +29,6 @@ from .uiformatters import AutonumberOverflowException from .filter_by_col import filter_by_collection from .auditlog import auditlog -from .datamodel import datamodel from .calculated_fields import calculate_extra_fields ReadPermChecker = Callable[[Any], None] @@ -581,7 +580,7 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List dirty: List[FieldChangeInfo] = [] for field_name, val in items: field = obj._meta.get_field(field_name) - if not field.many_to_one and not field.one_to_one: continue + if not field.many_to_one: continue old_related = get_related_or_none(obj, field_name) dependent = is_dependent_field(obj, field_name) @@ -601,15 +600,28 @@ def handle_fk_fields(collection, agent, obj, data: Dict[str, Any]) -> Tuple[List elif isinstance(val, str): # The related object is given by a URI reference. assert not dependent, "didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val) - fk_model, fk_id = strict_uri_to_model(val, field.related_model.__name__) + fk_model, fk_id = parse_uri(val) + assert fk_model == field.related_model.__name__.lower() + assert fk_id is not None setattr(obj, field_name, get_object_or_404(fk_model, id=fk_id)) new_related_id = fk_id elif hasattr(val, 'items'): # i.e. it's a dict of some sort # The related object is represented by a nested dict of data. + assert dependent, "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) rel_model = field.related_model - - rel_obj = update_or_create_resource(collection, agent, rel_model, val, obj if dependent else None) + if 'id' in val: + # The related object is an existing resource with an id. + # This should never happen. + rel_obj = update_obj(collection, agent, + rel_model, val['id'], + val['version'], val, + parent_obj=obj) + else: + # The related object is to be created. + rel_obj = create_obj(collection, agent, + rel_model, val, + parent_obj=obj) setattr(obj, field_name, rel_obj) if dependent and old_related and old_related.id != rel_obj.id: @@ -636,102 +648,39 @@ def handle_to_many(collection, agent, obj, data: Dict[str, Any]) -> None: for field_name, val in list(data.items()): field = obj._meta.get_field(field_name) if not field.is_relation or (field.many_to_one or field.one_to_one): continue # Skip *-to-one fields. - dependent = is_dependent_field(obj, field_name) - if isinstance(val, list): - assert dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'), \ - "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) - elif hasattr(val, "items"): - assert not dependent, "got inline dictionary data for dependent field %s in %s: %r" % (field_name, obj, val) - else: - # The field contains something other than nested data. - # Probably the URI of the collection + if isinstance(val, list): + assert isinstance(obj, models.Recordset) or obj.specify_model.get_field(field_name).dependent, \ + "got inline data for non dependent field %s in %s: %r" % (field_name, obj, val) + else: + # The field contains something other than nested data. + # Probably the URI of the collection of objects. + assert not obj.specify_model.get_field(field_name).dependent, \ + "didn't get inline data for dependent field %s in %s: %r" % (field_name, obj, val) continue - if dependent or (isinstance(obj, models.Recordset) and field_name == 'recordsetitems'): - _handle_dependent_to_many(collection, agent, obj, field, val) - else: - _handle_independent_to_many(collection, agent, obj, field, val) - -def _handle_dependent_to_many(collection, agent, obj, field, value): - if not isinstance(value, list): - assert isinstance(value, list), "didn't get inline data for dependent field %s in %s: %r" % (field.name, obj, value) - - rel_model = field.related_model - ids = [] # Ids not in this list will be deleted (if dependent) or removed from obj (if independent) at the end. - - for rel_data in value: - rel_data[field.field.name] = obj - - rel_obj = update_or_create_resource(collection, agent, rel_model, rel_data, parent_obj=obj) - - ids.append(rel_obj.id) # Record the id as one to keep. - - # Delete related objects not in the ids list. - # TODO: Check versions for optimistic locking. - to_remove = getattr(obj, field.name).exclude(id__in=ids).select_for_update() - for rel_obj in to_remove: - check_table_permissions(collection, agent, rel_obj, "delete") - auditlog.remove(rel_obj, agent, obj) - - to_remove.delete() - -class IndependentInline(TypedDict): - update: List[Union[str, Dict[str, Any]]] - remove: List[str] - -def _handle_independent_to_many(collection, agent, obj, field, value: IndependentInline): - logger.warning("Updating independent collections via the API is experimental and the structure may be changed in the future") - - rel_model = field.related_model - - to_update = value.get('update', []) - to_remove = value.get('remove', []) - - ids_to_fetch = [] - cached_objs = dict() - fk_model = None - - to_fetch = [*to_update, *to_remove] - - # Fetch the related records which are provided as strings - for rel_data in to_fetch: - if not isinstance(rel_data, str): continue - fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) - ids_to_fetch.append(fk_id) - - if fk_model is not None: - cached_objs = {item.id: obj_to_data(item) for item in get_model(fk_model).objects.filter(id__in=ids_to_fetch).select_for_update()} - - for rel_data in to_update: - if isinstance(rel_data, str): - fk_model, fk_id = strict_uri_to_model(rel_data, rel_model.__name__) - rel_data = cached_objs[fk_id] - if rel_data[field.field.name] == uri_for_model(obj.__class__, obj.id): - continue - - rel_data[field.field.name] = obj - update_or_create_resource(collection, agent, rel_model, rel_data, None) - - if len(to_remove) > 0: - assert obj.pk is not None, f"Unable to remove {obj.__class__.__name__}.{field.field.name} resources from new {obj.__class__.__name__}" - related_field = datamodel.reverse_relationship(obj.specify_model.get_field_strict(field.name)) - assert related_field is not None, f"no reverse relationship for {obj.__class__.__name__}.{field.field.name}" - for rel_obj in to_remove: - fk_model, fk_id = strict_uri_to_model(rel_obj, rel_model.__name__) - rel_data = cached_objs[fk_id] - assert rel_data[field.field.name] == uri_for_model(obj.__class__, obj.pk), f"Related {related_field.relatedModelName} does not belong to {obj.__class__.__name__}.{field.field.name}: {rel_obj}" - rel_data[field.field.name] = None - update_obj(collection, agent, rel_model, rel_data["id"], rel_data["version"], rel_data) - -def update_or_create_resource(collection, agent, model, data, parent_obj): - if 'id' in data: - return update_obj(collection, agent, - model, data['id'], - data['version'], data, - parent_obj=parent_obj) - else: - return create_obj(collection, agent, model, data, parent_obj=parent_obj) + rel_model = field.related_model + ids = [] # Ids not in this list will be deleted at the end. + for rel_data in val: + rel_data[field.field.name] = obj + if 'id' in rel_data: + # Update an existing related object. + rel_obj = update_obj(collection, agent, + rel_model, rel_data['id'], + rel_data['version'], rel_data, + parent_obj=obj) + else: + # Create a new related object. + rel_obj = create_obj(collection, agent, rel_model, rel_data, parent_obj=obj) + ids.append(rel_obj.id) # Record the id as one to keep. + + # Delete related objects not in the ids list. + # TODO: Check versions for optimistic locking. + to_delete = getattr(obj, field_name).exclude(id__in=ids) + for rel_obj in to_delete: + check_table_permissions(collection, agent, rel_obj, "delete") + auditlog.remove(rel_obj, agent, obj) + to_delete.delete() @transaction.atomic def delete_resource(collection, agent, name, id, version) -> None: @@ -838,12 +787,6 @@ def parse_uri(uri: str) -> Tuple[str, str]: groups = match.groups() return groups[0], groups[2] -def strict_uri_to_model(uri: str, model: str) -> Tuple[str, int]: - uri_model, uri_id = parse_uri(uri) - assert model.lower() == uri_model.lower(), f"{model} does not match model in uri: {uri_model}" - assert uri_id is not None - return uri_model, int(uri_id) - def obj_to_data(obj) -> Dict[str, Any]: "Wrapper for backwards compat w/ other modules that use this function." # TODO: Such functions should be audited for whether they should apply diff --git a/specifyweb/specify/datamodel.py b/specifyweb/specify/datamodel.py index 366d6b76385..b445e89b28a 100644 --- a/specifyweb/specify/datamodel.py +++ b/specifyweb/specify/datamodel.py @@ -8297,7 +8297,6 @@ Relationship(name='collection', type='many-to-one', required=False, relatedModelName='Collection', column='CollectionID'), Relationship(name='cogType', type='many-to-one', required=True, relatedModelName='CollectionObjectGroupType', column='COGTypeID'), Relationship(name='parentCojo', type='many-to-one', required=False, relatedModelName='CollectionObjectGroupJoin',column='CollectionObjectGroupJoinID', otherSideName='collectionobjectgroup'), - Relationship(name='cojo', type='one-to-many', required=False, relatedModelName='CollectionObjectGroupJoin', otherSideName='childCog', dependent=True), Relationship(name='children', type='one-to-many', required=False, dependent=True, relatedModelName='CollectionObjectGroupJoin', otherSideName='parentCog'), Relationship(name='createdByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='CreatedByAgentID'), Relationship(name='modifiedByAgent', type='many-to-one', required=False, relatedModelName='Agent', column='ModifiedByAgentID'), diff --git a/specifyweb/specify/tests/test_api.py b/specifyweb/specify/tests/test_api.py index 23ac6c17255..3a8a704e614 100644 --- a/specifyweb/specify/tests/test_api.py +++ b/specifyweb/specify/tests/test_api.py @@ -481,9 +481,7 @@ def test_update_object_with_more_inlines(self): even_dets = [d for d in data['determinations'] if d['number1'] % 2 == 0] for d in even_dets: data['determinations'].remove(d) - text1_data = 'look! an attribute' - - data['collectionobjectattribute'] = {'text1': text1_data} + data['collectionobjectattribute'] = {'text1': 'look! an attribute'} api.update_obj(self.collection, self.agent, 'collectionobject', data['id'], data['version'], data) @@ -493,172 +491,9 @@ def test_update_object_with_more_inlines(self): for d in obj.determinations.all(): self.assertFalse(d.number1 % 2 == 0) - self.assertEqual(obj.collectionobjectattribute.text1, text1_data) - - def test_independent_to_many_set_inline(self): - accession_data = { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': { - "update": [ - api.obj_to_data(self.collectionobjects[0]), - api.uri_for_model('collectionobject', self.collectionobjects[1].id) - ] - } - } - - accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) - self.collectionobjects[0].refresh_from_db() - self.collectionobjects[1].refresh_from_db() - self.assertEqual(accession, self.collectionobjects[0].accession) - self.assertEqual(accession, self.collectionobjects[1].accession) - - def test_independent_to_one_set_inline(self): - collection_object_data = { - 'collection': api.uri_for_model('collection', self.collection.id), - 'accession': { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id), - } - } - - created_co = api.create_obj(self.collection, self.agent, 'Collectionobject', collection_object_data) - self.assertIsNotNone(created_co.accession) - - def test_indepenent_to_many_removing_from_inline(self): - accession = models.Accession.objects.create( - accessionnumber="a", - version="0", - division=self.division - ) - - accession.collectionobjects.set(self.collectionobjects) - - self.assertEqual(accession, self.collectionobjects[0].accession) - - collection_objects_to_remove = [self.collectionobjects[0], self.collectionobjects[3]] - - cos_to_keep = [collection_object for collection_object in self.collectionobjects if not collection_object in collection_objects_to_remove] - - accession_data = { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': { - "remove": [ - api.uri_for_model('collectionobject', collection_object.id) - for collection_object in collection_objects_to_remove - ] - } - } - accession = api.update_obj(self.collection, self.agent, 'Accession', accession.id, accession.version, accession_data) - - self.assertEqual(list(accession.collectionobjects.all()), cos_to_keep) - - # ensure the other CollectionObjects have not been deleted - self.assertEqual(len(models.Collectionobject.objects.all()), len(self.collectionobjects)) - - def test_updating_independent_to_many_resource(self): - co_to_modify = api.obj_to_data(self.collectionobjects[2]) - co_to_modify.update({ - 'integer1': 10, - 'determinations': [ - { - 'iscurrent': True, - 'collectionmemberid': self.collection.id, - 'collectionobject': api.uri_for_model('Collectionobject', self.collectionobjects[2].id) - } - ] - }) - - accession_data = { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': { - "update": [ - co_to_modify - ] - } - } - - self.assertEqual(self.collectionobjects[2].integer1, None) - self.assertEqual(list(self.collectionobjects[2].determinations.all()), []) - accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) - self.collectionobjects[2].refresh_from_db() - self.assertEqual(self.collectionobjects[2].integer1, 10) - self.assertEqual(len(self.collectionobjects[2].determinations.all()), 1) - - def test_updating_independent_to_one_resource(self): - accession_data = { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id) - } - accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) - - accession_text = 'someText' - accession_data.update({ - 'id': accession.id, - 'accessionnumber': "a1", - 'text1': accession_text, - 'version': accession.version - }) + self.assertEqual(obj.collectionobjectattribute.text1, 'look! an attribute') - collection_object_data = { - 'collection': api.uri_for_model('collection', self.collection.id), - 'accession': accession_data - } - - self.assertEqual(accession.text1, None) - self.assertEqual(accession.accessionnumber, 'a') - created_co = api.create_obj(self.collection, self.agent, 'Collectionobject', collection_object_data) - accession.refresh_from_db() - self.assertEqual(accession.text1, accession_text) - self.assertEqual(accession.accessionnumber, 'a1') - - def test_independent_to_many_creating_from_remoteside(self): - new_catalognumber = f'num-{len(self.collectionobjects)}' - accession_data = { - 'accessionnumber': "a", - 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': { - "update": [ - { - 'catalognumber': new_catalognumber, - 'collection': api.uri_for_model('Collection', self.collection.id) - } - ] - } - } - - accession = api.create_obj(self.collection, self.agent, 'Accession', accession_data) - self.assertTrue(models.Collectionobject.objects.filter(catalognumber=new_catalognumber).exists()) - - def test_reassigning_independent_to_many(self): - acc1 = models.Accession.objects.create( - accessionnumber="a", - division = self.division - ) - self.collectionobjects[0].accession = acc1 - self.collectionobjects[0].save() - self.collectionobjects[1].accession = acc1 - self.collectionobjects[1].save() - - accession_data = { - 'accessionnumber': "b", - 'division': api.uri_for_model('division', self.division.id), - 'collectionobjects': { - "update": [ - api.obj_to_data(self.collectionobjects[0]), - api.uri_for_model('collectionobject', self.collectionobjects[1].id) - ] - } - } - acc2 = api.create_obj(self.collection, self.agent, 'Accession', accession_data) - self.collectionobjects[0].refresh_from_db() - self.collectionobjects[1].refresh_from_db() - self.assertEqual(self.collectionobjects[0].accession, acc2) - self.assertEqual(self.collectionobjects[1].accession, acc2) - # version control on inlined resources should be tested From cf2c79fdfd4fbe89125d4c01ff44f0ac1b9d7ece Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 1 Nov 2024 14:19:05 -0400 Subject: [PATCH 131/132] Update datamodel --- .../tests/ajax/static/context/datamodel.json | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json index b636b094142..0047279abbc 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/datamodel.json @@ -18290,7 +18290,7 @@ { "name": "discipline", "type": "many-to-one", - "required": true, + "required": false, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -18822,7 +18822,7 @@ { "name": "discipline", "type": "many-to-one", - "required": true, + "required": false, "dependent": false, "relatedModelName": "Discipline", "column": "DisciplineID", @@ -38444,6 +38444,14 @@ "relatedModelName": "CollectionObjectGroupJoin", "otherSideName": "childCog" }, + { + "name": "children", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "CollectionObjectGroupJoin", + "otherSideName": "parentCog" + }, { "name": "createdByAgent", "type": "many-to-one", @@ -38603,7 +38611,7 @@ "dependent": false, "relatedModelName": "CollectionObjectGroup", "column": "ParentCOGID", - "otherSideName": "parentcojos" + "otherSideName": "children" }, { "name": "childCog", @@ -38885,6 +38893,14 @@ "relatedModelName": "CollectionObject", "column": "CollectionObjectID" }, + { + "name": "absoluteAgeCitations", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "AbsoluteAgeCitation", + "otherSideName": "absoluteAge" + }, { "name": "createdByAgent", "type": "many-to-one", @@ -39116,6 +39132,14 @@ "relatedModelName": "CollectionObject", "column": "CollectionObjectID" }, + { + "name": "relativeAgeCitations", + "type": "one-to-many", + "required": false, + "dependent": true, + "relatedModelName": "RelativeAgeCitation", + "otherSideName": "relativeAge" + }, { "name": "createdByAgent", "type": "many-to-one", @@ -39408,7 +39432,8 @@ "required": true, "dependent": false, "relatedModelName": "AbsoluteAge", - "column": "AbsoluteAgeID" + "column": "AbsoluteAgeID", + "otherSideName": "absoluteAgeCitations" }, { "name": "collectionMember", @@ -39560,7 +39585,8 @@ "required": true, "dependent": false, "relatedModelName": "RelativeAge", - "column": "RelativeAgeID" + "column": "RelativeAgeID", + "otherSideName": "relativeAgeCitations" } ], "fieldAliases": [] @@ -40043,6 +40069,11 @@ "otherSideName": "treeEntries" } ], - "fieldAliases": [] + "fieldAliases": [ + { + "vname": "acceptedParent", + "aname": "acceptedTectonicUnit" + } + ] } ] \ No newline at end of file From f0483612d8b0667d6b691efb638f30b3cac15255 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 1 Nov 2024 15:56:57 -0400 Subject: [PATCH 132/132] Fix business rule when childCo is undefined --- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index bcf469717ff..fe237e9aa11 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -213,7 +213,11 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { const cojos = cog.getDependentResource('children'); // Set first CO in COG to primary cojos?.models - .find((cojo) => cojo.get('childCo') !== null) + .find( + (cojo) => + cojo.get('childCo') !== null && + cojo.get('childCo') !== undefined + ) ?.set('isPrimary', true); } });