diff --git a/specifyweb/backend/context/tests/test_system_info.py b/specifyweb/backend/context/tests/test_system_info.py new file mode 100644 index 00000000000..eb267c1ffd2 --- /dev/null +++ b/specifyweb/backend/context/tests/test_system_info.py @@ -0,0 +1,61 @@ +import json +from unittest.mock import patch + +from django.test import Client + +from specifyweb.specify.models import Spversion +from specifyweb.specify.tests.test_api import ApiTests + + +class TestSystemInfo(ApiTests): + def test_system_info_during_guided_setup_returns_safe_payload(self): + c = Client() + c.force_login(self.specifyuser) + c.cookies['collection'] = str(self.collection.id) + + with patch('specifyweb.backend.context.views.is_guided_setup_complete', return_value=False): + response = c.get('/context/system_info.json') + + self._assertStatusCodeEqual(response, 200) + payload = json.loads(response.content.decode()) + + self.assertFalse(payload['setup_complete']) + self.assertIsNone(payload['database_version']) + self.assertIsNone(payload['schema_version']) + self.assertIsNone(payload['institution']) + self.assertIsNone(payload['institution_guid']) + self.assertIsNone(payload['discipline']) + self.assertIsNone(payload['collection']) + self.assertIsNone(payload['collection_guid']) + self.assertIsNone(payload['isa_number']) + self.assertIsNone(payload['discipline_type']) + self.assertIsNone(payload['geography_is_global']) + self.assertIn('version', payload) + self.assertIn('specify6_version', payload) + + def test_system_info_after_guided_setup_returns_full_payload(self): + Spversion.objects.create(appversion='7', schemaversion='2.10') + + c = Client() + c.force_login(self.specifyuser) + c.cookies['collection'] = str(self.collection.id) + + with patch('specifyweb.backend.context.views.is_guided_setup_complete', return_value=True): + response = c.get('/context/system_info.json') + + self._assertStatusCodeEqual(response, 200) + payload = json.loads(response.content.decode()) + + self.assertTrue(payload['setup_complete']) + self.assertEqual(payload['database_version'], '7') + self.assertEqual(payload['schema_version'], '2.10') + self.assertEqual(payload['institution'], self.institution.name) + self.assertEqual(payload['institution_guid'], self.institution.guid) + self.assertEqual(payload['discipline'], self.discipline.name) + self.assertEqual(payload['collection'], self.collection.collectionname) + self.assertEqual(payload['collection_guid'], self.collection.guid) + self.assertEqual(payload['isa_number'], self.collection.isanumber) + self.assertEqual(payload['discipline_type'], self.discipline.type) + self.assertEqual( + payload['geography_is_global'], self.institution.issinglegeographytree + ) diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index 57d176ea3b9..4970bc6dd17 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -40,7 +40,8 @@ from specifyweb.backend.setup_tool.api import ( get_config_progress, filter_ready_disciplines_for_config_tasks, - is_collection_available + is_collection_available, + is_guided_setup_complete, ) def set_collection_cookie(response, collection_id): # pragma: no cover @@ -713,27 +714,53 @@ def _build_system_data(*, filter_not_ready_collections: bool): @skip_collection_access_check def system_info(request): "Return various information about this Specify instance." - spversion = Spversion.objects.get() - collection = request.specify_collection - discipline = collection.discipline if collection is not None else None - institution = Institution.objects.get() + setup_complete = is_guided_setup_complete() + + spversion = Spversion.objects.first() + collection = None + discipline = None + if setup_complete: + try: + collection = request.specify_collection + except Collection.DoesNotExist: + collection = None + else: + discipline = collection.discipline if collection is not None else None + institution = Institution.objects.first() + + database_version = spversion.appversion if spversion is not None else None + schema_version = spversion.schemaversion if spversion is not None else None + + institution_name = institution.name if institution is not None else None + institution_guid = institution.guid if institution is not None else None + geography_is_global = ( + institution.issinglegeographytree if institution is not None else None + ) + + if not setup_complete: + database_version = None + schema_version = None + institution_name = None + institution_guid = None + geography_is_global = None info = dict( version=settings.VERSION, specify6_version=re.findall(r'SPECIFY_VERSION=(.*)', specify_jar.read('resources_en.properties').decode('utf-8'))[0], - database_version=spversion.appversion, - schema_version=spversion.schemaversion, + setup_complete=setup_complete, + database_version=database_version, + schema_version=schema_version, stats_url=settings.STATS_URL, stats_2_url=settings.STATS_2_URL, database=settings.DATABASE_NAME, - institution=institution.name, - institution_guid=institution.guid, - discipline=discipline and discipline.name, - collection=collection and collection.collectionname, - collection_guid=collection and collection.guid, - isa_number=collection and collection.isanumber, - discipline_type=discipline and discipline.type, - geography_is_global=institution.issinglegeographytree + institution=institution_name, + institution_guid=institution_guid, + discipline=discipline.name if setup_complete and discipline is not None else None, + collection=collection.collectionname if setup_complete and collection is not None else None, + collection_guid=collection.guid if setup_complete and collection is not None else None, + isa_number=collection.isanumber if setup_complete and collection is not None else None, + discipline_type=discipline.type if setup_complete and discipline is not None else None, + geography_is_global=geography_is_global, ) return HttpResponse(json.dumps(info), content_type='application/json') diff --git a/specifyweb/backend/setup_tool/api.py b/specifyweb/backend/setup_tool/api.py index 906bfc4b8ff..6af46a60374 100644 --- a/specifyweb/backend/setup_tool/api.py +++ b/specifyweb/backend/setup_tool/api.py @@ -81,6 +81,11 @@ def get_setup_progress() -> dict: def _setup_resources_complete(completed_resources: dict) -> bool: return all(bool(resource_ready) for resource_ready in completed_resources.values()) + +def is_guided_setup_complete() -> bool: + """Return whether all Guided Setup resources have been created.""" + return _setup_resources_complete(get_setup_resource_progress()) + def get_setup_resource_progress() -> dict: """Returns a dictionary of the status of database setup resources.""" return { diff --git a/specifyweb/frontend/js_src/lib/components/Core/VersionMismatch.tsx b/specifyweb/frontend/js_src/lib/components/Core/VersionMismatch.tsx index eee50ce3396..d0e7676f19f 100644 --- a/specifyweb/frontend/js_src/lib/components/Core/VersionMismatch.tsx +++ b/specifyweb/frontend/js_src/lib/components/Core/VersionMismatch.tsx @@ -12,9 +12,12 @@ import { Dialog } from '../Molecules/Dialog'; * For databases created in Specify 7 this check is currently unnessecary. */ export function VersionMismatch(): JSX.Element | null { + const systemInfo = getSystemInfo(); const [showVersionMismatch, setShowVersionMismatch] = React.useState( - getSystemInfo().specify6_version !== getSystemInfo().database_version && - getSystemInfo().database_version !== '7' + systemInfo.setup_complete && + systemInfo.database_version !== null && + systemInfo.specify6_version !== systemInfo.database_version && + systemInfo.database_version !== '7' ); return showVersionMismatch ? (

{mainText.versionMismatchDescription({ - specifySixVersion: getSystemInfo().specify6_version, - databaseVersion: getSystemInfo().database_version, + specifySixVersion: systemInfo.specify6_version, + databaseVersion: systemInfo.database_version ?? '', })}

{mainText.versionMismatchSecondDescription()}

diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts index 40d3fec4695..3dcf374e1b0 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/__tests__/systemInfo.test.ts @@ -10,11 +10,15 @@ test('can fetch and parse system information', async () => database: 'specify', database_version: '6.8.03', discipline: 'Ichthyology', + discipline_type: 'fish', + geography_is_global: true, institution: 'University of Kansas Biodiversity Institute', institution_guid: '77ff1bff-af23-4647-b5d1-9d3c414fd003', isa_number: '2014427', schema_version: '2.9', + setup_complete: true, specify6_version: '6.8.03', + stats_2_url: 'https://stats-2.specifycloud.org', stats_url: 'https://sp7-stats.specifycloud.org/capture', version: '(debug)', })); diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index d3fe0a77da7..12e8c63ee13 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -104,14 +104,14 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { const parameters = { version: systemInfo.version, - dbVersion: systemInfo.database_version, - institution: systemInfo.institution, - institutionGUID: systemInfo.institution_guid, - discipline: systemInfo.discipline, - collection: systemInfo.collection, - collectionGUID: systemInfo.collection_guid, - isaNumber: systemInfo.isa_number, - disciplineType: systemInfo.discipline_type, + dbVersion: systemInfo.database_version ?? '', + institution: systemInfo.institution ?? '', + institutionGUID: systemInfo.institution_guid ?? '', + discipline: systemInfo.discipline ?? '', + collection: systemInfo.collection ?? '', + collectionGUID: systemInfo.collection_guid ?? '', + isaNumber: systemInfo.isa_number ?? '', + disciplineType: systemInfo.discipline_type ?? '', collectionObjectCount: counts?.Collectionobject ?? 0, collectionCount: counts?.Collection ?? 0, userCount: counts?.Specifyuser ?? 0, @@ -132,13 +132,13 @@ export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { ).catch(softFail); const lambdaUrl = buildStatsLambdaUrl(stats2LambdaFunctionUrl); - if (lambdaUrl) { + if (lambdaUrl && systemInfo.collection_guid) { if (shouldSkipLambdaStatsRequest(globalThis.location.hostname)) { return; } const storageKey = buildStats2RequestKey( lambdaUrl, - `${systemInfo.collection_guid}` + systemInfo.collection_guid ); if (!shouldSendStats2Request(storageKey)) { return; diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 253a19213d6..89eee28d1bc 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -9,19 +9,20 @@ import { load } from './index'; type SystemInfo = { readonly version: LocalizedString; readonly specify6_version: LocalizedString; - readonly database_version: LocalizedString; - readonly schema_version: LocalizedString; - readonly collection: string; - readonly collection_guid: LocalizedString; + readonly setup_complete: boolean; + readonly database_version: LocalizedString | null; + readonly schema_version: LocalizedString | null; + readonly collection: string | null; + readonly collection_guid: LocalizedString | null; readonly database: string; - readonly discipline: string; - readonly institution: string; - readonly institution_guid: LocalizedString; - readonly isa_number: LocalizedString; + readonly discipline: string | null; + readonly institution: string | null; + readonly institution_guid: LocalizedString | null; + readonly isa_number: LocalizedString | null; readonly stats_url: string | null; readonly stats_2_url: string | null; - readonly discipline_type: string; - readonly geography_is_global: string; + readonly discipline_type: string | null; + readonly geography_is_global: boolean | null; }; let systemInfo: SystemInfo; diff --git a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx index 5ce450c7622..aa89c72dae1 100644 --- a/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx +++ b/specifyweb/frontend/js_src/lib/components/SystemConfigurationTool/Hierarchy.tsx @@ -461,7 +461,7 @@ export function Hierarchy({ const loading = React.useContext(LoadingContext); - const isGeographyGlobal = systemInfo.geography_is_global; + const isGeographyGlobal = systemInfo.geography_is_global ?? false; const [ disciplineCreationOpen, diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx index 4fcb171b1f5..d520bd17b37 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx @@ -97,13 +97,15 @@ export function CreateTree< SpecifyResource | undefined >(undefined); - const connectedCollection = getSystemInfo().collection; - const discipline = getSystemInfo().discipline; + const connectedCollection = getSystemInfo().collection ?? ''; + const discipline = getSystemInfo().discipline ?? ''; + const canCreateTrees = discipline.trim().length > 0; // Start default tree creation const handleClick = async ( resource: TaxonFileDefaultDefinition ): Promise => { + if (!canCreateTrees) return; setIsTreeCreationStarted(true); return startTreeCreation( resource.file, @@ -233,13 +235,15 @@ export function ImportTree({ TaxonFileDefaultDefinition | undefined >(undefined); - const connectedCollection = getSystemInfo().collection; - const connectedDiscipline = getSystemInfo().discipline; + const connectedCollection = getSystemInfo().collection ?? ''; + const connectedDiscipline = getSystemInfo().discipline ?? ''; + const canCreateTrees = connectedDiscipline.trim().length > 0; const handleClick = async ( resource: TaxonFileDefaultDefinition, createMissingRanks?: boolean ): Promise => { + if (!canCreateTrees) return; setSelectedPopulatedTree(resource); // Check for missing ranks if no preference for createMissingRanks was provided. if (createMissingRanks === undefined) { @@ -374,7 +378,7 @@ export function ImportTree({ async function startTreeCreation( url: string, mappingUrl: string, - collection: string, + collectionName: string, disciplineName: string, rowCount: number, treeName: string, @@ -388,7 +392,7 @@ async function startTreeCreation( body: { url, mappingUrl, - collection, + collectionName, disciplineName, rowCount, treeName, diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts index fbc359e53b4..b31e5e53752 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts @@ -138,7 +138,10 @@ function navigator({ isTreeTable(table.name) && !valueIsTreeMeta(parentPartName); const disciplineType = getSystemInfo().discipline_type?.toLowerCase(); - const isNonGeoDiscipline = !geoPaleoDisciplines.includes(disciplineType); + const isGeoPaleoDiscipline = + disciplineType !== undefined && + geoPaleoDisciplines.includes(disciplineType); + const isNonGeoDiscipline = !isGeoPaleoDiscipline; if ( isNonGeoDiscipline && @@ -560,10 +563,10 @@ export function getMappingLineData({ const disciplineType = getSystemInfo().discipline_type?.toLowerCase(); - if ( - field.name === 'age' && - !geoPaleoDisciplines.includes(disciplineType) - ) { + const isGeoPaleoDiscipline = + disciplineType !== undefined && + geoPaleoDisciplines.includes(disciplineType); + if (field.name === 'age' && !isGeoPaleoDiscipline) { return false; } diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json index b328c94e680..ba0a419d52b 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/system_info.json @@ -1,14 +1,18 @@ { "version": "(debug)", "specify6_version": "6.8.03", + "setup_complete": true, "database_version": "6.8.03", "schema_version": "2.9", "stats_url": "https://sp7-stats.specifycloud.org/capture", + "stats_2_url": "https://stats-2.specifycloud.org", "database": "specify", "institution": "University of Kansas Biodiversity Institute", "institution_guid": "77ff1bff-af23-4647-b5d1-9d3c414fd003", "discipline": "Ichthyology", "collection": "KU Fish Voucher Collection", "collection_guid": "3f55b3fa-292d-4170-bd46-66dca41d7f05", - "isa_number": "2014427" + "isa_number": "2014427", + "discipline_type": "fish", + "geography_is_global": true }