Skip to content

Commit

Permalink
Merge branch 'issue-5154-patch' into issue-5131
Browse files Browse the repository at this point in the history
  • Loading branch information
grantfitzsimmons authored Feb 4, 2025
2 parents c2b53a3 + a0c1fdf commit 13d3532
Show file tree
Hide file tree
Showing 51 changed files with 1,633 additions and 1,106 deletions.
15 changes: 11 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,26 @@ RUN apt-get update \
ca-certificates \
curl \
git \
libldap2-dev \
libmariadbclient-dev \
libsasl2-dev \
libsasl2-modules \
libldap2-dev \
libssl-dev \
libgmp-dev \
libffi-dev \
python3.8-venv \
python3.8-distutils \
python3.8-dev
python3.8-dev \
libmariadbclient-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

USER specify
COPY --chown=specify:specify requirements.txt /home/specify/

WORKDIR /opt/specify7
RUN python3.8 -m venv ve \
&& ve/bin/pip install --no-cache-dir -r /home/specify/requirements.txt
&& ve/bin/pip install --no-cache-dir --upgrade pip setuptools wheel \
&& ve/bin/pip install -v --no-cache-dir -r /home/specify/requirements.txt
RUN ve/bin/pip install --no-cache-dir gunicorn

COPY --from=build-frontend /home/node/dist specifyweb/frontend/static/js
Expand Down
899 changes: 617 additions & 282 deletions LICENSE

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ questions you may have about configuration or deployment.

| Version | Supported |
| ------- | ------------------ |
| 7.10.x | :white_check_mark: |
| 7.9.x | :white_check_mark: |
| 7.8.x | :white_check_mark: |
| < 7.8 | :x: |
| < 7.8.x | :x: |

We support the latest version of Specify 6 only. You can report vulnerabilities
or other issues for that application on the
Expand All @@ -28,9 +28,9 @@ or other issues for that application on the
### Specify 7

Please contact [[email protected]](mailto:[email protected])
immediately if you encounter any security vulnerability in Specify 7 in addition
to creating a
[new bug report](https://github.com/specify/specify7/issues/new?assignees=&labels=type%3Abug%2C+pri%3Aunknown&template=bug_report.md&title=)
immediately if you encounter any security vulnerability in Specify 7.

If it is **not** a security vulnerability, you can create a [new bug report](https://github.com/specify/specify7/issues/new?assignees=&labels=type%3Abug%2C+pri%3Aunknown&template=bug_report.md&title=)
in the GitHub repository where it will be reviewed by the development team
within 24 hours.

Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Django==3.2.15
mysqlclient==2.1.1
SQLAlchemy==1.2.11
requests==2.32.2
pycryptodome==3.15.0
pycryptodome==3.21.0
PyJWT==2.3.0
django-auth-ldap==1.2.15
django-auth-ldap==1.2.17
jsonschema==3.2.0
typing-extensions==4.3.0
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.')

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
21 changes: 20 additions & 1 deletion specifyweb/businessrules/tests/test_cojo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from specifyweb.specify.models import Collectionobjectgroup, Collectionobjectgroupjoin, Collectionobjectgrouptype, Picklist, Picklistitem
from specifyweb.specify.models import Collectionobjectgroup, Collectionobjectgroupjoin, Collectionobjectgrouptype
from specifyweb.businessrules.exceptions import BusinessRuleException
from specifyweb.specify.tests.test_api import DefaultsSetup

class CoJoTest(DefaultsSetup):
Expand Down Expand Up @@ -58,3 +59,21 @@ def test_cojo_rules_enforcement(self):
self.assertTrue(cojo_2.issubstrate)
self.assertFalse(cojo_3.isprimary)
self.assertFalse(cojo_3.issubstrate)

with self.assertRaises(BusinessRuleException):
Collectionobjectgroupjoin.objects.create(
isprimary=False,
issubstrate=False,
parentcog=cog_1,
childcog=cog_4,
childco=self.collectionobjects[0]
)

with self.assertRaises(BusinessRuleException):
Collectionobjectgroupjoin.objects.create(
isprimary=False,
issubstrate=False,
parentcog=cog_1,
childcog=None,
childco=None
)
1 change: 1 addition & 0 deletions specifyweb/frontend/js_src/lib/components/Core/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type MenuItem = {
readonly enabled?: () => Promise<boolean> | boolean;
readonly icon: JSX.Element;
readonly name: string;
readonly onClick?: () => Promise<void>;
};

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,41 @@ describe('Collection Object business rules', () => {
};
overrideAjax(otherCollectionObjectTypeUrl, otherCollectionObjectType);

test('CollectionObject -> determinations: Save blocked when a determination does not belong to COT tree', async () => {
const collectionObject = getBaseCollectionObject();
collectionObject.set(
'collectionObjectType',
getResourceApiUrl('CollectionObjectType', 1)
);

const determination =
collectionObject.getDependentResource('determinations')?.models[0];

const { result } = renderHook(() =>
useSaveBlockers(determination, tables.Determination.getField('Taxon'))
);

await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});
expect(result.current[0]).toStrictEqual([
resourcesText.invalidDeterminationTaxon(),
]);

collectionObject.set(
'collectionObjectType',
getResourceApiUrl('CollectionObjectType', 2)
);
await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});
expect(result.current[0]).toStrictEqual([]);
});

test('CollectionObject -> determinations: New determinations are current by default', async () => {
const collectionObject = getBaseCollectionObject();
const determinations =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { resourcesText } from '../../localization/resources';
import { f } from '../../utils/functools';
import type { BusinessRuleResult } from './businessRules';
import {
COG_PRIMARY_KEY,
COG_TOITSELF,
CURRENT_DETERMINATION_KEY,
DETERMINATION_TAXON_KEY,
ensureSingleCollectionObjectCheck,
hasNoCurrentDetermination,
} from './businessRuleUtils';
Expand All @@ -19,7 +19,6 @@ import {
updateLoanPrep,
} from './interactionBusinessRules';
import type { SpecifyResource } from './legacyTypes';
import { fetchResource, idFromUrl } from './resource';
import { setSaveBlockers } from './saveBlockers';
import { schema } from './schema';
import type { Collection } from './specifyTable';
Expand Down Expand Up @@ -170,38 +169,31 @@ export const businessRuleDefs: MappedBusinessRuleDefs = {
},
fieldChecks: {
collectionObjectType: async (resource): Promise<undefined> => {
/*
* TEST: write tests for this
* Delete all determinations
*/
const determinations = resource.getDependentResource('determinations');
const currentDetermination = determinations?.models.find(
(determination) => determination.get('isCurrent')
if (determinations === undefined || determinations.models.length === 0)
return;

const taxons = await Promise.all(
determinations.models.map(async (det) => det.rgetPromise('taxon'))
);
const coType = await resource.rgetPromise('collectionObjectType');
const coTypeTreeDef = coType.get('taxonTreeDef');

const taxonId = idFromUrl(currentDetermination?.get('taxon') ?? '');
const COTypeID = idFromUrl(resource.get('collectionObjectType') ?? '');
if (
taxonId !== undefined &&
COTypeID !== undefined &&
currentDetermination !== undefined &&
determinations !== undefined
)
await f
.all({
fetchedTaxon: fetchResource('Taxon', taxonId),
fetchedCOType: fetchResource('CollectionObjectType', COTypeID),
})
.then(({ fetchedTaxon, fetchedCOType }) => {
const taxonTreeDefinition = fetchedTaxon.definition;
const COTypeTreeDefinition = fetchedCOType.taxonTreeDef;
// Block save when a Determination -> Taxon does not belong to the COType's tree definition
determinations.models.forEach((determination, index) => {
const taxon = taxons[index];
const taxonTreeDef = taxon?.get('definition');
const isValid =
typeof taxonTreeDef === 'string' && taxonTreeDef === coTypeTreeDef;

setSaveBlockers(
determination,
determination.specifyTable.field.taxon,
isValid ? [] : [resourcesText.invalidDeterminationTaxon()],
DETERMINATION_TAXON_KEY
);
});

if (taxonTreeDefinition !== COTypeTreeDefinition)
resource.set('determinations', []);
})
.catch((error) => {
console.error('Error fetching resources:', error);
});
return undefined;
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CURRENT_DETERMINATION_KEY = 'determination-isCurrent';
export const COG_TOITSELF = 'cog-toItself';
export const PARENTCOG_KEY = 'cog-parentCog';
export const COG_PRIMARY_KEY = 'cog-isPrimary';
export const DETERMINATION_TAXON_KEY = 'determination-Taxon';

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const fetchRows = async <
readonly fields: FIELDS;
readonly distinct?: boolean;
readonly limit?: number;
readonly filterChronostrat?: boolean;
},
/**
* Advanced filters, not type-safe.
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
Loading

0 comments on commit 13d3532

Please sign in to comment.