Skip to content

Improve CollectionRelationship uploads in the WorkBench #3240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
372b7ba
Add default forms for CollectionRelationship and CollectionRelType
melton-jason Mar 17, 2023
5967b20
Add schema overrides for CollectionRelationship and CollectionRelType
melton-jason Mar 17, 2023
2679c7e
Make CollectionRelType name a front-end only PickList
melton-jason Mar 17, 2023
bf5b8ce
Allow manual override of collection for workbench uploads
melton-jason Mar 22, 2023
b347d2d
Merge branch 'production' into issue-3089
melton-jason Mar 22, 2023
740bd35
Automatically override scope for Collection Relationships
melton-jason Mar 22, 2023
184274d
Apply deferred scoping on a per-row basis
melton-jason Mar 24, 2023
047b6a4
Add code documentation
melton-jason Mar 24, 2023
ac7e48a
Add overrideScope in calls to UploadTable in tests
melton-jason Mar 24, 2023
218d14d
Use proper imports for models.Collection
melton-jason Mar 24, 2023
5f63f63
Merge branch 'production' into issue-3089
grantfitzsimmons Mar 27, 2023
43015d8
Add remaining methods to DeferredScopeUploadTable from UploadTable
melton-jason Mar 27, 2023
28dc7f3
Merge branch 'issue-3089' of https://github.com/specify/specify7 into…
melton-jason Mar 27, 2023
48d6367
Truly make overrideScope optional in UploadTable and UnuploadTable
melton-jason Mar 27, 2023
6f5210a
Add test for inferring/parsing Deferred-Scope Upload Table
melton-jason Mar 29, 2023
21418bc
Remove direct 'toOne' attribute access
melton-jason Mar 29, 2023
076cb1d
Make rightSide unique in CollectionRelationship to Collectionreltype
melton-jason Mar 29, 2023
56dc2a5
Always use specified leftsidecollection from collectionreltype when u…
melton-jason Mar 31, 2023
623718c
Finish collection relationship workbench tests
melton-jason Mar 31, 2023
a45cc14
Merge branch 'production' into issue-3089
melton-jason Apr 5, 2023
827ff61
Fix backend test failing due to recursive type
melton-jason Apr 5, 2023
8d89155
Lint code with ESLint and Prettier
melton-jason Apr 5, 2023
a933fb7
Use type Any instead of NamedTuple in DeferredcopeUploadTable
melton-jason Apr 5, 2023
b57da15
Merge branch 'issue-3089' of https://github.com/specify/specify7 into…
melton-jason Apr 5, 2023
9d559c0
Fix most of failing backend tests
melton-jason Apr 5, 2023
5fb29b6
Use correct import for Spdataset
melton-jason Apr 5, 2023
04b1ea6
Fix more failing type check tests
melton-jason Apr 7, 2023
690376b
Merge branch 'production' into issue-3089
melton-jason Apr 7, 2023
b4c3ea3
Lint code with ESLint and Prettier
melton-jason Apr 7, 2023
7456aef
Fix more failing mypy type tests
melton-jason Apr 7, 2023
ed1eabb
Fix remaining type errors
melton-jason Apr 9, 2023
4a2fef6
Cast type of `scoped` in method bind() to ScopedUploadTable
melton-jason Apr 9, 2023
2289525
Ensure type of ScopedUploadTable is ScopedUploadTable after disambigu…
melton-jason Apr 9, 2023
7092b24
Use typing.cast() rather than variable type assignment
melton-jason Apr 9, 2023
a431c5b
Call UploadTestsBase setUp in ScopingTests setUp
melton-jason Apr 9, 2023
f45f6d2
Remove erroneous super().setUp() at end of ScopingTests setup()
melton-jason Apr 9, 2023
c01fcd0
Merge branch 'production' into issue-3089
melton-jason May 18, 2023
5fdc90b
Lint code with ESLint and Prettier
melton-jason May 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions specifyweb/businessrules/uniqueness_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def check_unique(instance):
'Collectionobject': {
'catalognumber': ['collection'],
},
'Collectionrelationship' : {
'rightside' : ['collectionreltype']
},
'Collector': {
'agent': ['collectingevent'],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ const globalFieldOverrides: {
Attachment: {
tableID: 'optional',
},
CollectionRelationship: {
collectionRelType: 'required',
},
CollectionRelType: {
name: 'required',
},
Taxon: {
parent: 'required',
isAccepted: 'readOnly',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function QueryComboBox({
typeof resource.getDependentResource(field.name) === 'object')
? resource
.rgetPromise<string, AnySchema>(field.name)
.then((resource) =>
.then(async (resource) =>
resource === undefined || resource === null
? {
label: '' as LocalizedString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PrintOnSave } from '../FormFields/Checkbox';
import type { ViewDescription } from '../FormParse';
import { SubViewContext } from '../Forms/SubView';
import { isTreeResource } from '../InitialContext/treeRanks';
import { interactionTables } from '../Interactions/config';
import { Dialog } from '../Molecules/Dialog';
import {
ProtectedAction,
Expand All @@ -33,7 +34,6 @@ import { QueryTreeUsages } from './QueryTreeUsages';
import { ReadOnlyMode } from './ReadOnlyMode';
import { ShareRecord } from './ShareRecord';
import { SubViewMeta } from './SubViewMeta';
import { interactionTables } from '../Interactions/config';

/**
* Form preferences host context aware user preferences and other meta-actions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ export const webOnlyViews = f.store(() =>
'spReports',
])
),
CollectionRelType: autoGenerateViewDefinition(
schema.models.CollectionRelType,
'form',
'edit',
['name', 'leftSideCollection', 'rightSideCollection', 'remarks']
),
CollectionRelationship: autoGenerateViewDefinition(
schema.models.CollectionRelationship,
'form',
'edit',
['collectionRelType', 'leftSide', 'rightSide']
),
[spAppResourceView]: autoGenerateViewDefinition(
schema.models.SpAppResource,
'form',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ function RecordSet<SCHEMA extends AnySchema>({
}).then(({ totalCount }) => totalCount !== 0),
})
)
).then((results) => {
).then(async (results) => {
const [nonDuplicates, duplicates] = split(
results,
({ isDuplicate }) => isDuplicate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ export const getFrontEndPickLists = f.store<{
.set('tableName', 'preptype')
.set('fieldName', 'name'),
},
CollectionRelType: {
name: definePicklist('_CollectionRelType', [])
.set('type', PickListTypes.FIELDS)
.set('tableName', 'collectionreltype')
.set('fieldName', 'name'),
},
SpAppResource: {
mimeType: definePicklist(
'_MimeType',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { RA } from '../../utils/types';
import { ensure } from '../../utils/types';
import { error } from '../Errors/assert';
import type { StatLayout } from '../Statistics/types';
import { GenericPreferences, defineItem } from './types';
import type { GenericPreferences } from './types';
import { defineItem } from './types';

export const collectionPreferenceDefinitions = {
statistics: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';

import { useLiveState } from '../../hooks/useLiveState';
import type { AppResourceTab } from '../AppResources/TabDefinitions';
import { PreferencesContent } from '../Preferences';
import { BasePreferences } from '../Preferences/BasePreferences';
import { userPreferenceDefinitions } from '../Preferences/UserDefinitions';
import { userPreferences } from '../Preferences/userPreferences';
import { AppResourceTab } from '../AppResources/TabDefinitions';
import { useLiveState } from '../../hooks/useLiveState';

export const UserPreferencesEditor: AppResourceTab = function ({
isReadOnly,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import { rawMenuItemsPromise } from '../Header/menuItemDefinitions';
import { useMenuItems, useUserTools } from '../Header/menuItemProcessing';
import { AttachmentPicker } from '../Molecules/AttachmentPicker';
import { AutoComplete } from '../Molecules/AutoComplete';
import { userPreferences } from './userPreferences';
import { ListEdit } from '../Toolbar/QueryTablesEdit';
import { PreferenceItem, PreferenceItemComponent } from './types';
import type { PreferenceItem, PreferenceItemComponent } from './types';
import { userPreferences } from './userPreferences';

export const ColorPickerPreferenceItem: PreferenceItemComponent<string> =
function ColorPickerPreferenceItem({
Expand Down
4 changes: 2 additions & 2 deletions specifyweb/frontend/js_src/lib/components/Security/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ function UserView({
status: Http.NO_CONTENT,
})
)
.then(({ data, status }) =>
.then(async ({ data, status }) =>
status === Http.BAD_REQUEST
? setState({
type: 'SettingAgents',
Expand Down Expand Up @@ -515,7 +515,7 @@ function UserView({
})
: true
)
.then((canContinue) =>
.then(async (canContinue) =>
canContinue === true
? Promise.all([
typeof password === 'string' && password !== ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { theories } from '../../../tests/utils';
import type { RA } from '../../../utils/types';
import type { AutoMapperResults } from '../autoMapper';
import {
AutoMapper as AutoMapperConstructor,
type AutoMapperConstructorParameters,
AutoMapper as AutoMapperConstructor,
circularTables,
} from '../autoMapper';

Expand Down
38 changes: 34 additions & 4 deletions specifyweb/workbench/upload/scoping.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Any, Union, Callable
from typing import Dict, Any, Optional, Tuple, Callable, Union

from specifyweb.specify.datamodel import datamodel, Table, Relationship
from specifyweb.specify.load_datamodel import DoesNotExistError
Expand All @@ -7,11 +7,38 @@
from specifyweb.stored_queries.format import get_date_format

from .uploadable import Uploadable, ScopedUploadable
from .upload_table import UploadTable, ScopedUploadTable, OneToOneTable, ScopedOneToOneTable
from .upload_table import UploadTable, DeferredScopeUploadTable, ScopedUploadTable, OneToOneTable, ScopedOneToOneTable
from .tomany import ToManyRecord, ScopedToManyRecord
from .treerecord import TreeRecord, ScopedTreeRecord
from .column_options import ColumnOptions, ExtendedColumnOptions

""" There are cases in which the scoping of records should be dependent on another record/column in a WorkBench dataset.

The DEFERRED_SCOPING dictonary defines the information needed to extract the correct scope to upload/validate a record into.

The structure of DEFERRED_SCOPING is as following:
The keys are tuples containing the django table name and a relationship that should be scoped.

The values are tuples containing the table name, field to filter on, and value to pull from that field to use as the collection for the
tableName.fieldName in the associated key of DEFERRED_SCOPING

For example, consider the following the deferred scoping information:
("Collectionrelationship", "rightside"): ('collectionreltype', 'name', 'rightsidecollection')

This information describes the following process to be performed:

'when uploading the rightside of a Collection Relationship, get the Collection Rel Type in the database from the dataset by
filtering Collection Rel Types in the database by name. Then, set the collection of the Collectionrelationship rightside equal to the Collection Rel Type's
rightSideCollection'

See .upload_plan_schema.py for how this is used

"""
DEFERRED_SCOPING: Dict[Tuple[str, str], Tuple[str, str, str]] = {
("Collectionrelationship", "rightside"): ('collectionreltype', 'name', 'rightsidecollection'),
("Collectionrelationship", "leftside"): ('collectionreltype', 'name', 'leftsidecollection'),
}

def scoping_relationships(collection, table: Table) -> Dict[str, int]:
extra_static: Dict[str, int] = {}

Expand Down Expand Up @@ -80,11 +107,14 @@ def extend_columnoptions(colopts: ColumnOptions, collection, tablename: str, fie
dateformat=get_date_format(),
)


def apply_scoping_to_uploadtable(ut: UploadTable, collection) -> ScopedUploadTable:
def apply_scoping_to_uploadtable(ut: Union[UploadTable, DeferredScopeUploadTable], collection) -> ScopedUploadTable:
table = datamodel.get_table_strict(ut.name)

adjust_to_ones = to_one_adjustments(collection, table)

if ut.overrideScope is not None and isinstance(ut.overrideScope['collection'], int):
collection = getattr(models, "Collection").objects.filter(id=ut.overrideScope['collection']).get()


return ScopedUploadTable(
name=ut.name,
Expand Down
6 changes: 6 additions & 0 deletions specifyweb/workbench/upload/tests/example_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def with_scoping(collection) -> ScopedUploadTable:
wbcols = {
'catalognumber' : parse_column_options("BMSM No."),
},
overrideScope=None,
static = {},
toMany = {
'determinations': [
Expand All @@ -160,6 +161,7 @@ def with_scoping(collection) -> ScopedUploadTable:
'middleinitial': parse_column_options('Determiner 1 Middle Initial'),
'lastname': parse_column_options('Determiner 1 Last Name'),
},
overrideScope=None,
static = {'agenttype': 1},
toOne = {},
toMany = {},
Expand Down Expand Up @@ -188,6 +190,7 @@ def with_scoping(collection) -> ScopedUploadTable:
'startdate' : parse_column_options('Start Date Collected'),
'stationfieldnumber' : parse_column_options('Station No.'),
},
overrideScope=None,
static = {},
toOne = {
'locality': UploadTable(
Expand All @@ -197,6 +200,7 @@ def with_scoping(collection) -> ScopedUploadTable:
'latitude1': parse_column_options('Latitude1'),
'longitude1': parse_column_options('Longitude1'),
},
overrideScope=None,
static = {'srclatlongunit': 0},
toOne = {
'geography': TreeRecord(
Expand Down Expand Up @@ -227,6 +231,7 @@ def with_scoping(collection) -> ScopedUploadTable:
'middleinitial' : parse_column_options('Collector 1 Middle Initial'),
'lastname' : parse_column_options('Collector 1 Last Name'),
},
overrideScope=None,
static = {'agenttype': 1},
toOne = {},
toMany = {},
Expand All @@ -246,6 +251,7 @@ def with_scoping(collection) -> ScopedUploadTable:
'middleinitial' : parse_column_options('Collector 2 Middle Initial'),
'lastname' : parse_column_options('Collector 2 Last name'),
},
overrideScope=None,
static = {'agenttype': 1},
toOne = {},
toMany = {},
Expand Down
3 changes: 3 additions & 0 deletions specifyweb/workbench/upload/tests/testdisambiguation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_disambiguation(self) -> None:
plan = UploadTable(
name='Referencework',
wbcols={'title': parse_column_options('title')},
overrideScope=None,
static={'referenceworktype': 0},
toOne={},
toMany={'authors': [
Expand All @@ -43,6 +44,7 @@ def test_disambiguation(self) -> None:
wbcols={
'lastname': parse_column_options('author1')
},
overrideScope=None,
static={},
toOne={},
toMany={}
Expand All @@ -56,6 +58,7 @@ def test_disambiguation(self) -> None:
wbcols={
'lastname': parse_column_options('author2')
},
overrideScope=None,
static={},
toOne={},
toMany={}
Expand Down
Loading