Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
45d3fe0
Lint code with ESLint and Prettier
melton-jason Jan 13, 2025
9b71967
Alter required status for Geo fields in WB
melton-jason Jan 14, 2025
634b998
Add cojo businessrule to automatically set precedence
melton-jason Jan 14, 2025
7139a76
Support one-to-one relationships in the WB
melton-jason Jan 14, 2025
04b576e
Use BoundUploadTable for remoteToOne relationships
melton-jason Jan 16, 2025
79a2adc
Merge branch 'production' into issue-5418
melton-jason Jan 16, 2025
54c95f3
Lint code with ESLint and Prettier
melton-jason Jan 16, 2025
4bf9691
Add comment
melton-jason Jan 16, 2025
5466aea
Merge branch 'issue-5418' of https://github.com/specify/specify7 into…
melton-jason Jan 16, 2025
e00513f
Upload remoteToOne relationships as toMany
melton-jason Jan 16, 2025
2962a01
Merge branch 'production' into issue-5418
grantfitzsimmons Jan 16, 2025
1d14140
Merge remote-tracking branch 'origin/production' into issue-5418
melton-jason Jan 17, 2025
a8797d3
Lint code with ESLint and Prettier
melton-jason Jan 17, 2025
1d4bedf
Merge branch 'production' into issue-5418
melton-jason Jan 17, 2025
5c48fe1
Merge branch 'production' into issue-5418
melton-jason Jan 21, 2025
b1f7352
Revert remoteToOne changes in backend
melton-jason Jan 21, 2025
07dfcf2
Parse the remote side of one-to-ones as toMany on frontend
melton-jason Jan 21, 2025
3a0adb6
Remove unused imports
melton-jason Jan 21, 2025
eeadce6
Map built-in celery states to WB statuses
melton-jason Jan 21, 2025
7be6316
Renable cojo.childco and cojo.childcog parity businessrules
melton-jason Jan 21, 2025
d30f414
Use identity comparator over equality comparator
melton-jason Jan 21, 2025
05eafa2
Merge branch 'production' into issue-5418
melton-jason Jan 22, 2025
3d8b3b6
Lint code with ESLint and Prettier
melton-jason Jan 22, 2025
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
36 changes: 22 additions & 14 deletions specifyweb/businessrules/rules/cojo_rules.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import os
import sys
from enum import Enum

from django.db.models import Max

from specifyweb.businessrules.exceptions import BusinessRuleException
from specifyweb.businessrules.orm_signal_handler import orm_signal_handler
from specifyweb.specify.models import Collectionobjectgroupjoin


class COGType(Enum):
DISCRETE = "Discrete"
CONSOLIDATED = "Consolidated"
DRILL_CORE = "Drill Core"

def is_running_tests():
return any(module in sys.modules for module in ('pytest', 'unittest'))

@orm_signal_handler('pre_save', 'Collectionobjectgroupjoin')
def cojo_pre_save(cojo):
# Ensure the both the childcog and childco fields are not null.
# if cojo.childcog == None and cojo.childco == None:
# raise BusinessRuleException('Both childcog and childco cannot be null.')
if cojo.childcog is None and cojo.childco is None:
raise BusinessRuleException('Both childcog and childco cannot be null.')

# Ensure the childcog and childco fields are not both set.
# if cojo.childcog != None and cojo.childco != None:
# raise BusinessRuleException('Both childcog and childco cannot be set.')
if cojo.childcog is not None and cojo.childco is not None:
raise BusinessRuleException('Both childcog and childco cannot be set.')

# For records with the same parentcog field, there can be only one isPrimare field set to True.
# So when a record is saved with isPrimary set to True, we need to set all other records with the same parentcog
Expand All @@ -41,24 +40,33 @@ def cojo_pre_save(cojo):
cojo.childcog is not None
and cojo.childcog.cojo is not None
and cojo.childcog.cojo.id is not cojo.id
and not is_running_tests()
):
raise BusinessRuleException('ChildCog is already in use as a child in another COG.')
raise BusinessRuleException(
'ChildCog is already in use as a child in another COG.')

if (
cojo.childco is not None
and cojo.childco.cojo is not None
and cojo.childco.cojo.id is not cojo.id
and not is_running_tests()
):
raise BusinessRuleException('ChildCo is already in use as a child in another COG.')

raise BusinessRuleException(
'ChildCo is already in use as a child in another COG.')
Comment thread
melton-jason marked this conversation as resolved.

if cojo.precedence is None:
others = Collectionobjectgroupjoin.objects.filter(
parentcog=cojo.parentcog
)
top = others.aggregate(Max('precedence'))['precedence__max']
cojo.precedence = 0 if top is None else top + 1


@orm_signal_handler('post_save', 'Collectionobjectgroupjoin')
def cojo_post_save(cojo):
"""
For Consolidated COGs, mark the first CO child as primary if none have been set by the user
"""
co_children = Collectionobjectgroupjoin.objects.filter(parentcog=cojo.parentcog, childco__isnull=False)
co_children = Collectionobjectgroupjoin.objects.filter(
parentcog=cojo.parentcog, childco__isnull=False)
if len(co_children) > 0 and not co_children.filter(isprimary=True).exists() and cojo.parentcog.cogtype.type == COGType.CONSOLIDATED.value:
first_child = co_children.first()
first_child.isprimary = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,13 @@ const fieldOverwrites: typeof globalFieldOverrides = {
CollectionObject: {
collectionObjectType: { visibility: 'optional' },
},
CollectionObjectGroupType: {
type: { visibility: 'optional' },
},
CollectionObjectGroupJoin: {
precedence: { visibility: 'optional' },
isSubstrate: { visibility: 'optional' },
},
LoanPreparation: {
isResolved: { visibility: 'optional' },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type { Collection } from '../DataModel/specifyTable';
import { getTable } from '../DataModel/tables';
import type { PickList } from '../DataModel/types';
import { IntegratedRecordSelector } from '../FormSliders/IntegratedRecordSelector';
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';

export function PickListEditor({
resource,
Expand Down Expand Up @@ -56,8 +55,7 @@ export function PickListEditor({
relationship={relationship}
sortField={undefined}
onAdd={
relationshipIsToMany(relationship) &&
relationship.type !== 'zero-to-one'
relationship.type.includes('-to-many')
? undefined
: ([resource]): void =>
void resource.set(relationship.name, resource as never)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { SpecifyResource } from '../DataModel/legacyTypes';
import type { Relationship } from '../DataModel/specifyField';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { useSearchDialog } from '../SearchDialog';
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';
import { Slider } from './Slider';

export type RecordSelectorProps<SCHEMA extends AnySchema> = {
Expand Down Expand Up @@ -84,9 +83,7 @@ export function useRecordSelector<SCHEMA extends AnySchema>({
);

const isToOne =
field === undefined
? false
: !relationshipIsToMany(field) || field.type === 'zero-to-one';
field === undefined ? false : !field.type.includes('-to-many');

const handleResourcesSelected = React.useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { SpecifyResource } from '../DataModel/legacyTypes';
import { resourceOn } from '../DataModel/resource';
import type { Relationship } from '../DataModel/specifyField';
import type { Collection } from '../DataModel/specifyTable';
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';
import type {
RecordSelectorProps,
RecordSelectorState,
Expand Down Expand Up @@ -58,8 +57,7 @@ export function RecordSelectorFromCollection<SCHEMA extends AnySchema>({

const isDependent = collection instanceof DependentCollection;
const isLazy = collection instanceof LazyCollection;
const isToOne =
!relationshipIsToMany(relationship) || relationship.type === 'zero-to-one';
const isToOne = !relationship.type.includes('-to-many');

// Listen for changes to collection
React.useEffect(
Expand Down
7 changes: 4 additions & 3 deletions specifyweb/frontend/js_src/lib/components/Forms/SubView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,11 @@ export function SubView({

return (
<SubViewContext.Provider value={contextValue}>
{!RECURSIVE_RENDERING_EXCEPTIONS.has(parentResource.specifyTable) &&
{(!RECURSIVE_RENDERING_EXCEPTIONS.has(parentResource.specifyTable) &&
parentContext
.map(({ relationship }) => relationship)
.includes(relationship) || collection === false ? undefined : (
.map(({ relationship }) => relationship)
.includes(relationship)) ||
collection === false ? undefined : (
<>
{isButton && (
<Button.BorderedGray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type SystemInfo = {
readonly institution_guid: LocalizedString;
readonly isa_number: LocalizedString;
readonly stats_url: string | null;
readonly discipline_type: string
readonly discipline_type: string;
};

let systemInfo: SystemInfo;
Expand All @@ -45,7 +45,7 @@ export const fetchContext = load<SystemInfo>(
collection: systemInfo.collection,
collectionGUID: systemInfo.collection_guid,
isaNumber: systemInfo.isa_number,
disciplineType: systemInfo.discipline_type
disciplineType: systemInfo.discipline_type,
},
/*
* I don't know if the receiving server handles GET parameters in a
Expand Down
4 changes: 3 additions & 1 deletion specifyweb/frontend/js_src/lib/components/PickLists/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ async function fetchFromField(
fields: { [fieldName]: ['string', 'number', 'boolean', 'null'] },
distinct: true,
domainFilter: true,
filterChronostrat: tableName === tables.GeologicTimePeriod.name.toLowerCase() && fieldName === "name", // Prop for age filter in QueryBuilder
filterChronostrat:
tableName === tables.GeologicTimePeriod.name.toLowerCase() &&
fieldName === 'name', // Prop for age filter in QueryBuilder
}).then((rows) =>
rows
.map((row) => row[fieldName] ?? '')
Expand Down
60 changes: 32 additions & 28 deletions specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,41 +524,45 @@ export function QueryComboBox({
.map(serializeResource)
.map(({ fieldName, startValue }) =>
fieldName === 'rankId'
? {
field: 'rankId',
isRelationship: false,
isNot: false,
operation: 'less',
value: startValue,
}
: fieldName === 'nodeNumber'
? {
field: 'nodeNumber',
isRelationship: false,
operation: 'between',
isNot: true,
value: startValue,
}
: fieldName === 'collectionRelTypeId'
? {
field: 'id',
field: 'rankId',
isRelationship: false,
operation: 'in',
isNot: false,
operation: 'less',
value: startValue,
}
: fieldName === 'taxonTreeDefId'
: fieldName === 'nodeNumber'
? {
field: 'definition',
isRelationship: true,
operation: 'in',
isNot: false,
value: startValue
field: 'nodeNumber',
isRelationship: false,
operation: 'between',
isNot: true,
value: startValue,
}
: f.error(`extended filter not created`, {
fieldName,
startValue,
}))
: fieldName === 'collectionRelTypeId'
? {
field: 'id',
isRelationship: false,
operation: 'in',
isNot: false,
value: startValue,
}
: fieldName === 'taxonTreeDefId'
? {
field: 'definition',
isRelationship: true,
operation: 'in',
isNot: false,
value: startValue,
}
: f.error(
`extended filter not created`,
{
fieldName,
startValue,
}
)
)
),
})
: undefined
Expand Down
12 changes: 10 additions & 2 deletions specifyweb/frontend/js_src/lib/components/SearchDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ const filterResults = <SCHEMA extends AnySchema>(

function testFilter<SCHEMA extends AnySchema>(
resource: SpecifyResource<SCHEMA>,
{ operation, field, value, isNot, isRelationship }: QueryComboBoxFilter<SCHEMA>
{
operation,
field,
value,
isNot,
isRelationship,
}: QueryComboBoxFilter<SCHEMA>
): boolean {
const values = value.split(',').map(f.trim);
const result =
Expand All @@ -151,7 +157,9 @@ function testFilter<SCHEMA extends AnySchema>(
values.some((value) => {
const fieldValue = resource.get(field);
// eslint-disable-next-line eqeqeq
return isRelationship ? value == strictIdFromUrl(fieldValue!).toString() : value == fieldValue
return isRelationship
? value == strictIdFromUrl(fieldValue!).toString()
: value == fieldValue;
})
: operation === 'less'
? values.every((value) => (resource.get(field) ?? 0) < value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getNameFromTreeRankName,
getNumberFromToManyIndex,
mappingPathToString,
relationshipIsRemoteToOne,
relationshipIsToMany,
valueIsToManyIndex,
valueIsTreeRank,
Expand Down Expand Up @@ -809,7 +810,10 @@ export class AutoMapper {
.forEach((relationship) => {
const localPath = [...mappingPath, relationship.name];

if (relationshipIsToMany(relationship))
if (
relationshipIsToMany(relationship) ||
relationshipIsRemoteToOne(relationship)
)
localPath.push(formatToManyIndex(1));

const newDepthLevel = localPath.length;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
formatToManyIndex,
formatTreeRank,
mappingPathToString,
relationshipIsRemoteToOne,
relationshipIsToMany,
valueIsToManyIndex,
valueIsTreeRank,
Expand Down Expand Up @@ -266,10 +267,13 @@ export function mutateMappingPath({
const table = getTable(parentTableName ?? '');
const currentField = table?.getField(mappingPath[index] ?? '');
const isCurrentToMany =
currentField?.isRelationship === true && relationshipIsToMany(currentField);
currentField?.isRelationship === true &&
(relationshipIsToMany(currentField) ||
relationshipIsRemoteToOne(currentField));
const newField = table?.getField(newValue);
const isNewToMany =
newField?.isRelationship === true && relationshipIsToMany(newField);
newField?.isRelationship === true &&
(relationshipIsToMany(newField) || relationshipIsRemoteToOne(newField));
const isNewTree =
newField?.isRelationship === true &&
isTreeTable(newField.relatedTable.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ export const relationshipIsToMany = (
relationship?.type.includes('-to-many') === true ||
relationship?.type === 'zero-to-one';

/**
* Returns whether the relatation is one-to-one from the remote side
* (the foreign key exists on the other table of the relationship)
*
* In the WorkBench, remote one-to-one relationships are parsed as to-many
* in the upload plan
*
* See https://github.com/specify/specify7/pull/6073#discussion_r1915397675
*/
export const relationshipIsRemoteToOne = (
relationship: Relationship | undefined
): boolean =>
relationship?.type === 'one-to-one' &&
relationship.databaseColumn === undefined;

export type FieldType = Exclude<keyof CollectionObject, 'tableName'>;

/** Returns whether a value is a -to-many index (e.x #1, #2, etc...) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { MappingPath } from './Mapper';
import {
formatTreeRank,
getNumberFromToManyIndex,
relationshipIsRemoteToOne,
relationshipIsToMany,
valueIsToManyIndex,
valueIsTreeRank,
Expand Down Expand Up @@ -103,8 +104,10 @@ export function findRequiredMissingFields(
// Disable circular relationships
(isCircularRelationship(parentRelationship, relationship) ||
// Skip -to-many inside -to-many
(relationshipIsToMany(parentRelationship) &&
relationshipIsToMany(relationship)))
((relationshipIsToMany(parentRelationship) ||
relationshipIsRemoteToOne(parentRelationship)) &&
(relationshipIsToMany(relationship) ||
relationshipIsRemoteToOne(relationship))))
)
return [];

Expand Down
Loading