diff --git a/specifyweb/backend/context/urls.py b/specifyweb/backend/context/urls.py index d94248603b7..897db2a5271 100644 --- a/specifyweb/backend/context/urls.py +++ b/specifyweb/backend/context/urls.py @@ -21,6 +21,7 @@ re_path(r'^user.json$', views.user), re_path(r'^stats_counts.json$', views.stats_counts), re_path(r'^system_info.json$', views.system_info), + re_path(r'^all_system_data.json$', views.all_system_data), re_path(r'^server_time.json$', views.get_server_time), re_path(r'^domain.json$', views.domain), re_path(r'^view.json$', views.view), diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index a94f29eacfe..950bcb30fe2 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -26,7 +26,7 @@ PermissionTargetAction, \ check_permission_targets, skip_collection_access_check, query_pt, \ CollectionAccessPT -from specifyweb.specify.models import Collection, Collectionobject, Institution, \ +from specifyweb.specify.models import Collection, Discipline, Division, Collectionobject, Institution, \ Specifyuser, Spprincipal, Spversion, Collectionobjecttype from specifyweb.specify.models_utils.schema import base_schema from specifyweb.specify.models_utils.serialize_datamodel import datamodel_to_json @@ -663,10 +663,60 @@ def system_info(request): collection=collection and collection.collectionname, collection_guid=collection and collection.guid, isa_number=collection and collection.isanumber, - discipline_type=discipline and discipline.type + discipline_type=discipline and discipline.type, + geography_is_global=institution.issinglegeographytree ) return HttpResponse(json.dumps(info), content_type='application/json') +@require_http_methods(["GET"]) +@cache_control(max_age=86400, public=True) +@skip_collection_access_check +def all_system_data(request): + """ + Returns all institutions, divisions, disciplines, and collections. + """ + institution = Institution.objects.get() + divisions = list(Division.objects.all()) + disciplines = list(Discipline.objects.all()) + collections = list(Collection.objects.all()) + + discipline_map = {} + for discipline in disciplines: + discipline_map[discipline.id] = { + "id": discipline.id, + "name": discipline.name, + "children": [], + "geographytreedef": discipline.geographytreedef_id, + "taxontreedef": discipline.taxontreedef_id + } + + for collection in collections: + if collection.discipline_id in discipline_map: + discipline_map[collection.discipline_id]["children"].append({ + "id": collection.id, + "name": collection.collectionname + }) + + division_map = {} + for division in divisions: + division_map[division.id] = { + "id": division.id, + "name": division.name, + "children": [] + } + + for discipline in disciplines: + if discipline.division_id in division_map: + division_map[discipline.division_id]["children"].append(discipline_map[discipline.id]) + + institution_data = { + "id": institution.id, + "name": institution.name, + "children": list(division_map.values()) + } + + return JsonResponse(institution_data, safe=False) + PATH_GROUP_RE = re.compile(r'\(\?P<([^>]+)>[^\)]*\)') PATH_GROUP_RE_EXTENDED = re.compile(r'<([^:]+):([^>]+)>') diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts b/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts index 6d0c43f68d3..429ebcaeed1 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/webOnlyViews.ts @@ -84,6 +84,22 @@ export const webOnlyViews = f.store(() => 'edit', ['name'] ), + [collection]: autoGenerateViewDefinition( + tables.Collection, + 'form', + 'edit', + ['collectionName', 'code', 'catalogNumFormatName'] + ), + [division]: autoGenerateViewDefinition(tables.Division, 'form', 'edit', [ + 'name', + 'abbrev', + ]), + [discipline]: autoGenerateViewDefinition( + tables.Discipline, + 'form', + 'edit', + ['name', 'type'] + ), } as const) ); @@ -92,3 +108,6 @@ export const attachmentView = 'ObjectAttachment'; export const spAppResourceView = '_SpAppResourceView_name'; export const spViewSetNameView = '_SpViewSetObj_name'; export const recordSetView = '_RecordSet_name'; +export const collection = '_Collection_setup'; +export const division = '_Division_setup'; +export const discipline = '_Discipline_setup'; diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index 6855811b3b4..641e5832184 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -79,6 +79,11 @@ const rawUserTools = ensure>>>()({ url: '/specify/security/', icon: icons.fingerPrint, }, + systemConfigurationTool: { + title: userText.systemConfigurationTool(), + url: '/specify/system-configuration/', + icon: icons.fingerPrint, + }, repairTree: { title: headerText.repairTree(), url: '/specify/overlay/tree-repair/', diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 21518f41335..07294189d60 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -24,6 +24,7 @@ type SystemInfo = { readonly stats_url: string | null; readonly stats_2_url: string | null; readonly discipline_type: string; + readonly geography_is_global: string; }; type StatsCounts = { @@ -80,6 +81,7 @@ export const fetchContext = load( collectionObjectCount: counts?.Collectionobject ?? 0, collectionCount: counts?.Collection ?? 0, userCount: counts?.Specifyuser ?? 0, + geographyIsGlobal: systemInfo.geography_is_global, }; await ping( diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index e932a03d020..566b61db025 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -174,6 +174,14 @@ export const routes: RA = [ }, ], }, + { + path: 'system-configuration', + title: userText.securityPanel(), + element: () => + import('../SystemConfigurationTool/SystemConfigTool').then( + ({ SystemConfigurationTool }) => SystemConfigurationTool + ), + }, { path: 'attachments', children: [ diff --git a/specifyweb/frontend/js_src/lib/components/SetupTool/setupResources.ts b/specifyweb/frontend/js_src/lib/components/SetupTool/setupResources.ts index 8d00ac5dd47..4b1c89eb734 100644 --- a/specifyweb/frontend/js_src/lib/components/SetupTool/setupResources.ts +++ b/specifyweb/frontend/js_src/lib/components/SetupTool/setupResources.ts @@ -75,7 +75,8 @@ export const resources: RA = [ label: setupToolText.institution(), description: setupToolText.institutionDescription(), endpoint: '/setup_tool/institution/create/', - documentationUrl: 'https://discourse.specifysoftware.org/t/specify-setup-configuration-checklist/1056', + documentationUrl: + 'https://discourse.specifysoftware.org/t/specify-setup-configuration-checklist/1056', fields: [ { name: 'name', @@ -150,7 +151,8 @@ export const resources: RA = [ { name: 'isSingleGeographyTree', label: setupToolText.institutionIsSingleGeographyTree(), - description: setupToolText.institutionIsSingleGeographyTreeDescription(), + description: + setupToolText.institutionIsSingleGeographyTreeDescription(), type: 'boolean', default: false, }, diff --git a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/CollapsibleSection.tsx b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/CollapsibleSection.tsx new file mode 100644 index 00000000000..92be7f22dab --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/CollapsibleSection.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Button } from '../Atoms/Button'; + +export const CollapsibleSection = ({ + title, + children, + defaultOpen = true, + hasChildren, +}: { + title: React.ReactNode; + children: React.ReactNode; + defaultOpen?: boolean; + hasChildren: boolean; +}) => { + const [isOpen, _, __, handleOpen] = useBooleanState(defaultOpen); + + return ( +
+
+ + {title} +
+ + {isOpen &&
{children}
} +
+ ); +}; diff --git a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx new file mode 100644 index 00000000000..2a4e4a62b2f --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx @@ -0,0 +1,321 @@ +import React from 'react'; +import type { LocalizedString } from 'typesafe-i18n'; + +import { useBooleanState } from '../../hooks/useBooleanState'; +import { useId } from '../../hooks/useId'; +import { commonText } from '../../localization/common'; +import { setupToolText } from '../../localization/setupTool'; +import type { RA } from '../../utils/types'; +import { H2, H3, Ul } from '../Atoms'; +import { Button } from '../Atoms/Button'; +import { Form } from '../Atoms/Form'; +import { Link } from '../Atoms/Link'; +import { Submit } from '../Atoms/Submit'; +import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { tables } from '../DataModel/tables'; +import { getSystemInfo } from '../InitialContext/systemInfo'; +import { Dialog, LoadingScreen } from '../Molecules/Dialog'; +import { ResourceLink } from '../Molecules/ResourceLink'; +import type { ResourceFormData } from '../SetupTool'; +import { renderFormFieldFactory, stepOrder } from '../SetupTool'; +import { resources } from '../SetupTool/setupResources'; +import { CollapsibleSection } from './CollapsibleSection'; +import type { InstitutionData } from './Utils'; + +type DialogFormProps = { + readonly open: boolean; + readonly onClose: () => void; + readonly resourceIndex: number; + readonly title: LocalizedString; + readonly step: number; +}; + +function DialogForm({ open, onClose, title, step }: DialogFormProps) { + const id = useId('config-tool'); + + if (!open) return null; + + const formRef = React.useRef(null); + + const [formData, setFormData] = React.useState( + Object.fromEntries(stepOrder.map((key) => [key, {}])) + ); + + const [temporaryFormData, setTemporaryFormData] = + React.useState({}); + + const handleChange = (name: string, newValue: LocalizedString | boolean) => { + const resourceName = resources[5].resourceName; + setFormData((previous) => ({ + ...previous, + [resourceName]: { + ...previous[resourceName], + [name]: newValue, + }, + })); + }; + + const { renderFormFields } = renderFormFieldFactory({ + formData, + currentStep: 5, + handleChange, + temporaryFormData, + setTemporaryFormData, + formRef, + }); + + return ( + + {commonText.create()} + + {commonText.cancel()} + + } + header={title} + onClose={onClose} + > +
{}} + > + {renderFormFields(resources[step].fields)} +
+
+ ); +} + +const handleEditResource = ( + resource: SpecifyResource, + refreshAllInfo: () => Promise +) => ( + { + await refreshAllInfo(); + globalThis.location.reload(); + }, + onSaved: async () => { + await refreshAllInfo(); + globalThis.location.reload(); + }, + }} + /> +); + +const addButton = ( + createResource: () => void, + tableName: string +): JSX.Element => ( + { + createResource(); + }} + /> +); + +export function Hierarchy({ + institution, + setNewResource, + handleNewResource, + refreshAllInfo, +}: { + readonly institution: InstitutionData | null; + readonly setNewResource: (resource: any) => void; + readonly handleNewResource: () => void; + readonly refreshAllInfo: () => Promise; +}): JSX.Element { + if (!institution) return ; + + const systemInfo = getSystemInfo(); + + const isGeographyGlobal = systemInfo.geography_is_global; + + const [ + addDisciplineGeoTree, + openAddDisciplineGeoTree, + closeAddDisciplineGeoTree, + ] = useBooleanState(false); + const [ + addDisciplineTaxonTree, + openAddDisciplineTaxonTree, + closeAddDisciplineTaxonTree, + ] = useBooleanState(false); + + const renderCollections = ( + collections: RA<{ + readonly id: number; + readonly name: string; + }> + ) => ( + 0} + title={

Collections

} + > +
    + {collections.map((collection) => ( +
    +
  • +

    {collection.name}

    +
  • + {handleEditResource( + new tables.Collection.Resource({ id: collection.id }), + refreshAllInfo + )} +
    + ))} +
+
+ ); + + const renderDisciplines = (division: any) => + division.children.map((discipline: any) => { + const needsGeoTree = + isGeographyGlobal && typeof discipline.geographytreedef !== 'number'; + const needsTaxonTree = typeof discipline.taxontreedef !== 'number'; + const canAddCollection = !needsGeoTree && !needsTaxonTree; + + return ( +
  • + 0} + title={ +
    +

    {`Discipline: ${discipline.name}`}

    + {/* GEO TREE */} + {needsGeoTree && ( +
    + { + setNewResource(new tables.GeographyTreeDef.Resource()); + openAddDisciplineGeoTree(); + }} + /> +

    + {setupToolText.treeConfigurationWarning()} +

    +
    + )} + {/* TAXON TREE */} + {needsTaxonTree && ( +
    + { + setNewResource(new tables.TaxonTreeDef.Resource()); + openAddDisciplineTaxonTree(); + }} + /> +

    + {setupToolText.treeConfigurationWarning()} +

    +
    + )} + {handleEditResource( + new tables.Discipline.Resource({ id: discipline.id }), + refreshAllInfo + )} + + {/* ADD COLLECTION */} + {canAddCollection && + addButton(() => { + setNewResource( + new tables.Collection.Resource({ + discipline: `/api/specify/discipline/${discipline.id}/`, + }) + ); + handleNewResource(); + }, 'collection')} +
    + } + > + {/* COLLECTIONS */} + {discipline.children.length > 0 && + renderCollections(discipline.children)} + + {/* TREE CONFIG DIALOGS */} + {/* GEO */} + + + {/* TAXON */} + +
    +
  • + ); + }); + + const renderDivisions = (institution: InstitutionData) => + institution.children.map((division: any) => ( +
  • + 0} + title={ +
    +

    {`Division: ${division.name}`}

    + + {handleEditResource( + new tables.Division.Resource({ id: division.id }), + refreshAllInfo + )} + + {addButton(() => { + setNewResource( + new tables.Discipline.Resource({ + division: `/api/specify/division/${division.id}/`, + }) + ); + handleNewResource(); + }, 'Discipline')} +
    + } + > +
      {renderDisciplines(division)}
    +
    +
  • + )); + + return ( +
      +
    • + 0} + title={ +
      +

      {`Institution: ${institution.name}`}

      + {addButton(() => { + setNewResource(new tables.Division.Resource()); + handleNewResource(); + }, 'Division')} +
      + } + > +
        {renderDivisions(institution)}
      +
      +
    • +
    + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/SystemConfigTool.tsx new file mode 100644 index 00000000000..5df9afc1d22 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/SystemConfigTool.tsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import { useBooleanState } from '../../hooks/useBooleanState'; +import { commonText } from '../../localization/common'; +import { userText } from '../../localization/user'; +import { ajax } from '../../utils/ajax'; +import { localized } from '../../utils/types'; +import { toLowerCase } from '../../utils/utils'; +import { Container, H2 } from '../Atoms'; +import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { serializeResource } from '../DataModel/serializers'; +import type { + Collection, + Discipline, + Division, + GeographyTreeDef, + TaxonTreeDef, +} from '../DataModel/types'; +import { collection, discipline, division } from '../FormParse/webOnlyViews'; +import { ResourceView } from '../Forms/ResourceView'; +import { load } from '../InitialContext'; +import { Dialog, LoadingScreen } from '../Molecules/Dialog'; +import { Hierarchy } from './Hierarchy'; +import type { InstitutionData } from './Utils'; +import { fetchAllSystemData } from './Utils'; + +export function SystemConfigurationTool(): JSX.Element | null { + const [allInfo, setAllInfo] = React.useState(null); + + const [newResourceOpen, handleNewResource, closeNewResource] = + useBooleanState(); + + const [newResource, setNewResource] = React.useState< + | SpecifyResource + | SpecifyResource + | SpecifyResource + | SpecifyResource + | SpecifyResource + | undefined + >(); + + React.useEffect(() => { + fetchAllSystemData + .then(setAllInfo) + .catch(() => console.warn('Error when fetching institution info')); + }, []); + + const refreshAllInfo = async () => + load( + '/context/all_system_data.json', + 'application/json' + ).then(setAllInfo); + + const handleSaved = () => { + if (!newResource) return; + + const data = serializeResource(newResource as SpecifyResource); + + ajax<{}>( + `/setup_tool/${toLowerCase(newResource.specifyTable.name)}/create/`, + { + method: 'POST', + headers: { + Accept: 'application/json', + }, + body: data, + } + ) + .then(refreshAllInfo) + .then(closeNewResource); + }; + + return ( + +

    {userText.systemConfigurationTool()}

    +
    + {allInfo === undefined || allInfo === null ? ( + + ) : ( + + )} +
    + {newResourceOpen ? ( + + } + viewName={ + newResource?.specifyTable.name === 'Collection' + ? collection + : newResource?.specifyTable.name === 'Discipline' + ? discipline + : newResource?.specifyTable.name === 'Division' + ? division + : undefined + } + onAdd={undefined} + onClose={closeNewResource} + onDeleted={undefined} + onSaved={handleSaved} + /> + + ) : undefined} +
    + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Utils.tsx b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Utils.tsx new file mode 100644 index 00000000000..faa1e78be46 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Utils.tsx @@ -0,0 +1,36 @@ +import { type RA } from '../../utils/types'; +import { load } from '../InitialContext'; + +export type InstitutionData = { + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Division + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Discipline + readonly id: number; + readonly name: string; + readonly children: RA<{ + // Collection + readonly id: number; + readonly name: string; + }>; + readonly geographytreedef: number | null; + readonly taxontreedef: number | null; + }>; + }>; +}; + +let institutionData: InstitutionData; + +export const fetchAllSystemData = load( + '/context/all_system_data.json', + 'application/json' +).then((data: InstitutionData) => { + institutionData = data; + return data; +}); + +export const getAllInfo = (): InstitutionData => institutionData; diff --git a/specifyweb/frontend/js_src/lib/localization/setupTool.ts b/specifyweb/frontend/js_src/lib/localization/setupTool.ts index 68bbc37cce9..4cc80bbd9e6 100644 --- a/specifyweb/frontend/js_src/lib/localization/setupTool.ts +++ b/specifyweb/frontend/js_src/lib/localization/setupTool.ts @@ -32,7 +32,7 @@ export const setupToolText = createDictionary({ /** * Field Captions. * These must be defined here because schema captions don't exist during the setup. - */ + */ // Institution institution: { 'en-us': 'Institution', @@ -64,13 +64,15 @@ export const setupToolText = createDictionary({ 'en-us': 'Define Accession Globally', }, institutionIsAccessionGlobalDescription: { - 'en-us': 'Global scope allows you to share Accessions between all divisions. Divisional scope ensures Accessions are specific to each division.', + 'en-us': + 'Global scope allows you to share Accessions between all divisions. Divisional scope ensures Accessions are specific to each division.', }, institutionIsSingleGeographyTree: { 'en-us': 'Use Single Geography Tree', }, institutionIsSingleGeographyTreeDescription: { - 'en-us': 'A global geography tree is shared by all disciplines. Otherwise, geography trees are managed separately within each discipline.', + 'en-us': + 'A global geography tree is shared by all disciplines. Otherwise, geography trees are managed separately within each discipline.', }, // Address @@ -113,7 +115,7 @@ export const setupToolText = createDictionary({ // Trees treeRanks: { - 'en-us': 'Tree Ranks' + 'en-us': 'Tree Ranks', }, fullNameDirection: { 'en-us': 'Full Name Direction', @@ -134,9 +136,11 @@ export const setupToolText = createDictionary({ taxonTree: { 'en-us': 'Taxon Tree', }, - // defaultTree: { - // 'en-us': 'Pre-load Default Tree' - // }, + /* + * DefaultTree: { + * 'en-us': 'Pre-load Default Tree' + * }, + */ // Division division: { @@ -196,4 +200,19 @@ export const setupToolText = createDictionary({ specifyUserConfirmPasswordDescription: { 'en-us': 'Must match the password entered above.', }, + configGeoTree: { + 'en-us': 'Configure your geography tree for your discipline', + }, + configTaxonTree: { + 'en-us': 'Configure your taxon tree for your discipline', + }, + treeConfigurationWarning: { + 'en-us': 'Set up your tree', + }, + addNewGeographyTree: { + 'en-us': 'Add new Geography Tree', + }, + addNewTaxonTree: { + 'en-us': 'Add new Taxon Tree', + }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/user.ts b/specifyweb/frontend/js_src/lib/localization/user.ts index e1392acceb0..971606a9ae6 100644 --- a/specifyweb/frontend/js_src/lib/localization/user.ts +++ b/specifyweb/frontend/js_src/lib/localization/user.ts @@ -1097,4 +1097,7 @@ export const userText = createDictionary({ 'uk-ua': 'Додати користувача', 'pt-br': 'Adicionar usuário', }, + systemConfigurationTool: { + 'en-us': 'System Configuration Tool', + }, } as const); diff --git a/specifyweb/specify/models.py b/specifyweb/specify/models.py index 4e991d02ecb..8b986027fac 100644 --- a/specifyweb/specify/models.py +++ b/specifyweb/specify/models.py @@ -1376,7 +1376,7 @@ class Collection(models.Model): # Relationships: Many-to-One admincontact = models.ForeignKey('Agent', db_column='AdminContactID', related_name='+', null=True, on_delete=protect_with_blockers) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='collections', null=False, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='collections', null=False, on_delete=models.CASCADE) institutionnetwork = models.ForeignKey('Institution', db_column='InstitutionNetworkID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) collectionobjecttype = models.ForeignKey('CollectionObjectType', db_column='CollectionObjectTypeID', related_name='collections', null=True, on_delete=models.SET_NULL) @@ -2886,7 +2886,7 @@ class Discipline(model_extras.Discipline): # Relationships: Many-to-One createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) datatype = models.ForeignKey('DataType', db_column='DataTypeID', related_name='+', null=False, on_delete=protect_with_blockers) - division = models.ForeignKey('Division', db_column='DivisionID', related_name='disciplines', null=False, on_delete=protect_with_blockers) + division = models.ForeignKey('Division', db_column='DivisionID', related_name='disciplines', null=False, on_delete=models.CASCADE) geographytreedef = models.ForeignKey('GeographyTreeDef', db_column='GeographyTreeDefID', related_name='disciplines', null=False, on_delete=protect_with_blockers) taxontreedef = models.ForeignKey('TaxonTreeDef', db_column='TaxonTreeDefID', related_name='disciplines', null=True, on_delete=protect_with_blockers) geologictimeperiodtreedef = models.ForeignKey('GeologicTimePeriodTreeDef', db_column='GeologicTimePeriodTreeDefID', related_name='disciplines', null=False, on_delete=protect_with_blockers) @@ -3045,7 +3045,7 @@ class Division(models.Model): # Relationships: Many-to-One address = models.ForeignKey('Address', db_column='AddressID', related_name='divisions', null=True, on_delete=protect_with_blockers) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - institution = models.ForeignKey('Institution', db_column='InstitutionID', related_name='divisions', null=False, on_delete=protect_with_blockers) + institution = models.ForeignKey('Institution', db_column='InstitutionID', related_name='divisions', null=False, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) class Meta: @@ -3689,7 +3689,7 @@ class Geographytreedef(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='geographytreedefs', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='geographytreedefs', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -3795,7 +3795,7 @@ class Geologictimeperiodtreedef(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='geologictimeperiodtreedefs', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='geologictimeperiodtreedefs', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -4294,7 +4294,7 @@ class Lithostrattreedef(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='lithostratstreedefs', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='lithostratstreedefs', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -5093,7 +5093,7 @@ class Picklist(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - collection = models.ForeignKey('Collection', db_column='CollectionID', related_name='picklists', null=False, on_delete=protect_with_blockers) + collection = models.ForeignKey('Collection', db_column='CollectionID', related_name='picklists', null=False, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -5948,9 +5948,9 @@ class Spappresourcedir(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - collection = models.ForeignKey('Collection', db_column='CollectionID', related_name='+', null=True, on_delete=protect_with_blockers) + collection = models.ForeignKey('Collection', db_column='CollectionID', related_name='+', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='+', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='+', null=True, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) specifyuser = models.ForeignKey('SpecifyUser', db_column='SpecifyUserID', related_name='spappresourcedirs', null=True, on_delete=models.CASCADE) @@ -6039,7 +6039,7 @@ class Spexportschema(models.Model): # Relationships: Many-to-One createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='spexportschemas', null=False, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='spexportschemas', null=False, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) class Meta: @@ -6188,7 +6188,7 @@ class Splocalecontainer(models.Model): # Relationships: Many-to-One createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='splocalecontainers', null=False, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='splocalecontainers', null=False, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) class Meta: @@ -6222,7 +6222,7 @@ class Splocalecontaineritem(models.Model): weblinkname = models.CharField(blank=True, max_length=32, null=True, unique=False, db_column='WebLinkName', db_index=False) # Relationships: Many-to-One - container = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerID', related_name='items', null=False, on_delete=protect_with_blockers) + container = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerID', related_name='items', null=False, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -6252,11 +6252,11 @@ class Splocaleitemstr(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - containerdesc = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerDescID', related_name='descs', null=True, on_delete=protect_with_blockers) - containername = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerNameID', related_name='names', null=True, on_delete=protect_with_blockers) + containerdesc = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerDescID', related_name='descs', null=True, on_delete=models.CASCADE) + containername = models.ForeignKey('SpLocaleContainer', db_column='SpLocaleContainerNameID', related_name='names', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - itemdesc = models.ForeignKey('SpLocaleContainerItem', db_column='SpLocaleContainerItemDescID', related_name='descs', null=True, on_delete=protect_with_blockers) - itemname = models.ForeignKey('SpLocaleContainerItem', db_column='SpLocaleContainerItemNameID', related_name='names', null=True, on_delete=protect_with_blockers) + itemdesc = models.ForeignKey('SpLocaleContainerItem', db_column='SpLocaleContainerItemDescID', related_name='descs', null=True, on_delete=models.CASCADE) + itemname = models.ForeignKey('SpLocaleContainerItem', db_column='SpLocaleContainerItemNameID', related_name='names', null=True, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) class Meta: @@ -6486,7 +6486,7 @@ class Sptasksemaphore(models.Model): # Relationships: Many-to-One collection = models.ForeignKey('Collection', db_column='CollectionID', related_name='+', null=True, on_delete=protect_with_blockers) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='+', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='+', null=True, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) owner = models.ForeignKey('SpecifyUser', db_column='OwnerID', related_name='tasksemaphores', null=True, on_delete=protect_with_blockers) @@ -6701,7 +6701,7 @@ class Storagetreedef(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - institution = models.ForeignKey('Institution', db_column='InstitutionID', related_name='storagetreedefs', null=True, on_delete=protect_with_blockers) + institution = models.ForeignKey('Institution', db_column='InstitutionID', related_name='storagetreedefs', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) @@ -7130,7 +7130,7 @@ class Taxontreedef(models.Model): # Relationships: Many-to-One discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='taxontreedefs', null=True, on_delete=protect_with_blockers) - createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) + createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=models.CASCADE) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) class Meta: @@ -7909,7 +7909,7 @@ class Tectonicunittreedef(models.Model): version = models.IntegerField(blank=True, null=False, unique=False, db_column='Version', db_index=False, default=0) # Relationships: Many-to-One - discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='tectonicunittreedefs', null=True, on_delete=protect_with_blockers) + discipline = models.ForeignKey('Discipline', db_column='DisciplineID', related_name='tectonicunittreedefs', null=True, on_delete=models.CASCADE) createdbyagent = models.ForeignKey('Agent', db_column='CreatedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) modifiedbyagent = models.ForeignKey('Agent', db_column='ModifiedByAgentID', related_name='+', null=True, on_delete=protect_with_blockers) diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index fccdc0080a5..c8d525dfbeb 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -15,6 +15,7 @@ from django.conf import settings from django.views.decorators.cache import cache_control from django.views.decorators.http import require_http_methods +from django.db import IntegrityError from specifyweb.middleware.general import require_http_methods from specifyweb.specify.api.dispatch import collection_dispatch, resource_dispatch @@ -77,6 +78,9 @@ def view(request, *args, **kwargs): return http.HttpResponseBadRequest(e) except http.Http404 as e: return http.HttpResponseNotFound(e) + except IntegrityError as e: + ## catch the exact error for Field 'UserGroupScopeId' from e and the exact resource path fron request and continue to avoid current crash + return http.HttpResponseNotFound(e) return view