diff --git a/specifyweb/backend/context/urls.py b/specifyweb/backend/context/urls.py index 59ee69b16c1..5741f2cc6a2 100644 --- a/specifyweb/backend/context/urls.py +++ b/specifyweb/backend/context/urls.py @@ -20,6 +20,7 @@ re_path(r'^api_endpoints_all.json$', views.api_endpoints_all), re_path(r'^user.json$', views.user), 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 bed109ce506..76aedda2d82 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, Institution, \ +from specifyweb.specify.models import Collection, Discipline, Division, 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 @@ -666,6 +666,53 @@ def system_info(request): ) 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": [] + } + + 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/backend/setup_tool/views.py b/specifyweb/backend/setup_tool/views.py index d22efd8f4c2..59e2d1babd1 100644 --- a/specifyweb/backend/setup_tool/views.py +++ b/specifyweb/backend/setup_tool/views.py @@ -2,6 +2,21 @@ import json from specifyweb.backend.setup_tool import api +from specifyweb.specify import models + +def create_institution(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + new_institution = models.Institution.objects.create(**data) + return http.JsonResponse( + {"success": True, "institution_id": new_institution.id}, + status=201 + ) + except Exception as e: + print(f"Error creating institution: {e}") + return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) + return http.JsonResponse({"error": "Invalid request"}, status=400) def setup_database_view(request): return api.setup_database(request) 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/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index e932a03d020..37772579d25 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('../Toolbar/SystemConfigTool').then( + ({ SystemConfigurationTool }) => SystemConfigurationTool + ), + }, { path: 'attachments', children: [ diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx new file mode 100644 index 00000000000..44bbeea85e1 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Toolbar/SystemConfigTool.tsx @@ -0,0 +1,223 @@ +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 { type RA, localized } from '../../utils/types'; +import { toLowerCase } from '../../utils/utils'; +import { Container, H2, H3, Ul } from '../Atoms'; +import { Button } from '../Atoms/Button'; +import type { SpecifyResource } from '../DataModel/legacyTypes'; +import { serializeResource } from '../DataModel/serializers'; +import { tables } from '../DataModel/tables'; +import type { Collection, Discipline, Division } 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'; + +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 + | 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); + }; + + const renderHierarchy = ( + institution: InstitutionData | null + ): JSX.Element => { + if (!institution) return ; + + return ( +
    +
  • +
    +

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

    + { + console.log( + `'Add new division to institution' ${institution.id}` + ); + setNewResource(new tables.Division.Resource()); + handleNewResource(); + }} + /> +
    +
      + {institution.children.map((division) => ( +
    • +
      +

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

      + { + console.log( + `'Add new discipline to division' ${division.id}` + ); + setNewResource( + new tables.Discipline.Resource({ + division: `/api/specify/discipline/${division.id}/`, + }) + ); + handleNewResource(); + }} + /> +
      + {division.children.length > 0 && ( +
        + {division.children.map((discipline) => ( +
      • +
        +

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

        + { + console.log( + `'Add new collection to discipline' ${discipline.id}` + ); + setNewResource( + new tables.Collection.Resource({ + discipline: `/api/specify/discipline/${discipline.id}/`, + }) + ); + handleNewResource(); + }} + /> +
        + {discipline.children.length > 0 && ( +
          + {discipline.children.map((collection) => ( +
        • +

          {collection.name}

          +
        • + ))} +
        + )} +
      • + ))} +
      + )} +
    • + ))} +
    +
  • +
+ ); + }; + + const viewName = collection; + + return ( + +

{userText.systemConfigurationTool()}

+
+ {allInfo === undefined || allInfo === null ? ( + + ) : ( + renderHierarchy(allInfo) + )} +
+ {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} +
+ ); +} + +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; + }>; + }>; + }>; +}; + +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/user.ts b/specifyweb/frontend/js_src/lib/localization/user.ts index aaa1a4d9a18..67bc15b7ea1 100644 --- a/specifyweb/frontend/js_src/lib/localization/user.ts +++ b/specifyweb/frontend/js_src/lib/localization/user.ts @@ -1096,4 +1096,7 @@ export const userText = createDictionary({ 'uk-ua': 'Додати користувача', 'pt-br': 'Adicionar usuário', }, + systemConfigurationTool: { + 'en-us': 'System Configuration Tool', + }, } as const);