diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index ed50095bf07..53e3a98c03f 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -5,6 +5,7 @@ import json import os import re +import logging from typing import List from xml.etree import ElementTree @@ -14,7 +15,7 @@ from django.db import connection, transaction from django.db.models import Q from django.http import Http404, HttpResponse, HttpResponseBadRequest, \ - HttpResponseForbidden, JsonResponse + HttpResponseForbidden, HttpResponseServerError, JsonResponse from django.urls import URLPattern from django.utils.translation import get_language_info from django.utils.translation import gettext as _ @@ -46,6 +47,8 @@ is_collection_available, is_guided_setup_complete, ) + +logger = logging.getLogger(__name__) def set_collection_cookie(response, collection_id): # pragma: no cover response.set_cookie('collection', str(collection_id), max_age=365*24*60*60) @@ -655,13 +658,13 @@ def schema_localization_import(request): if not isinstance(language, str) or not re.fullmatch( r'[A-Za-z]{2}(?:-[A-Za-z]{2})?', language ): - raise ValueError + raise ValueError(f"Invalid language string: {language}") if isinstance(schema, dict) and {'language', 'schema'} <= schema.keys(): source_language = schema['language'] if not isinstance(source_language, str) or ( source_language.lower() != language.lower() ): - raise ValueError + raise ValueError(f"Invalid Localization (expected {source_language}, got {language}") schema = schema['schema'] references = { 'format': _schema_import_resource_names( @@ -704,8 +707,16 @@ def schema_localization_import(request): request.specify_collection, request.specify_user_agent, model.__name__, data ) - except (AttributeError, KeyError, TypeError, ValueError, json.JSONDecodeError): - return HttpResponseBadRequest() + except (AttributeError, ValueError, json.JSONDecodeError) as err: + logger.warning("Schema import validation failed", exc_info=True) + return JsonResponse( + {"error": "Invalid schema localization import payload."}, + status=400 + ) + except (KeyError, TypeError) as err: + logger.warning(f"Schema Import failed: {err}") + return HttpResponseServerError("An internal error has occurred.", content_type="text/plain") + return JsonResponse({'updated': len(operations)}) view_parameters_schema = [ diff --git a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Layout.tsx b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Layout.tsx index 161dfc818d4..ac53a147a8d 100644 --- a/specifyweb/frontend/js_src/lib/components/SchemaConfig/Layout.tsx +++ b/specifyweb/frontend/js_src/lib/components/SchemaConfig/Layout.tsx @@ -24,6 +24,7 @@ import { useSchemaConfig, } from './Store'; import { dialogIcons } from '../Atoms/Icons'; +import { raise } from '../Errors/Crash'; export function SchemaConfigLayout(): JSX.Element { const schemaData = useOutletContext(); @@ -55,8 +56,7 @@ function SchemaConfigLayoutContent(): JSX.Element { const setSingleResource = React.useContext(SetSingleResourceContext); const loading = React.useContext(LoadingContext); const [importFile, setImportFile] = React.useState(); - const [importError, setImportError] = React.useState(false); - const [importSuccessful, setImportSuccessful] = React.useState(false); + const [importStatus, setImportStatus] = React.useState(0); const [importRefreshError, setImportRefreshError] = React.useState(false); React.useEffect(() => { @@ -80,7 +80,6 @@ function SchemaConfigLayoutContent(): JSX.Element { ); }; const handleImport = (file: File): void => { - setImportError(false); setImportFile(file); }; const confirmImport = (): void => { @@ -95,17 +94,25 @@ function SchemaConfigLayoutContent(): JSX.Element { method: 'POST', headers: { Accept: 'application/json' }, body: { schema, language: rawLanguage }, - errorMode: 'silent', + expectedErrors: [400, 500, 504] }) ) .then( - () => setImportSuccessful(true), - () => setImportError(true) + (response) => { + setImportStatus(response.status); + if (response.status !== 200) + raise({name: schemaText.importSchemaError({schemaConfig: schemaText.schemaConfig()}), message: + response.status === 504 ? schemaText.importSchemaErrorTimeout({schemaConfig: schemaText.schemaConfig()}) : + response.status === 400 ? schemaText.importSchemaErrorBadRequest({schemaConfig: schemaText.schemaConfig()}) : + schemaText.importSchemaErrorInternalError() + }) + }, + (err) => raise(err) ) ); }; const closeImportSuccess = (): void => { - setImportSuccessful(false); + setImportStatus(0); loading( handleSchemaSaved(rawLanguage, tableName).catch(() => setImportRefreshError(true) @@ -170,25 +177,7 @@ function SchemaConfigLayoutContent(): JSX.Element {

)} - {importError && ( - {commonText.close()} - } - icon={dialogIcons.error} - header={schemaText.importSchema({ - schemaConfig: schemaText.schemaConfig(), - })} - onClose={(): void => setImportError(false)} - > -

- {schemaText.importSchemaError({ - schemaConfig: schemaText.schemaConfig(), - })} -

-
- )} - {importSuccessful && ( + {importStatus == 200 && ( {commonText.close()} diff --git a/specifyweb/frontend/js_src/lib/localization/schema.ts b/specifyweb/frontend/js_src/lib/localization/schema.ts index bb0d69c5555..208973c7397 100644 --- a/specifyweb/frontend/js_src/lib/localization/schema.ts +++ b/specifyweb/frontend/js_src/lib/localization/schema.ts @@ -11,141 +11,56 @@ import { createDictionary } from './utils'; export const schemaText = createDictionary({ importSchema: { 'en-us': 'Import {schemaConfig:string}', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaWarning: { 'en-us': 'Importing a {schemaConfig:string} will overwrite the current one. This action cannot be undone.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaLanguageWarning: { 'en-us': 'The exported schema language must match the language currently being configured.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaBackupPrompt: { 'en-us': 'We strongly recommend downloading a backup of the current {schemaConfig:string} before importing.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaLimitations: { 'en-us': 'This import will not assign pick lists, field formats, or web links unless they already exist.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, downloadSchemaBackup: { 'en-us': 'Export {schemaConfig:string}', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaContinue: { 'en-us': 'Continue Import', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaError: { + 'en-us': '{schemaConfig:string} import failed:', + }, + importSchemaErrorBadRequest: { 'en-us': 'The {schemaConfig:string} export is invalid or was exported for a different language and cannot be imported.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + }, + importSchemaErrorTimeout: { + 'en-us': + 'Network request timed out, Specify may or may not still import the {schemaConfig:string} export in the background. Please check back in later to see if your changes have been applied.', + }, + importSchemaErrorInternalError: { + 'en-us': + 'Specify experienced an error, please clear your cache and try again.', }, importSchemaSuccess: { 'en-us': '{schemaConfig:string} imported successfully', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaSuccessDescription: { 'en-us': 'The imported {schemaConfig:string} has been saved.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, importSchemaRefreshError: { 'en-us': 'The import succeeded, but the page could not refresh automatically. Reload the page to see the imported changes.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, reloadSchemaConfig: { 'en-us': 'Reload Page', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', }, table: { 'en-us': 'Table',