From 0c55979117353d6fb228b2582ec8ccf0579b05fc Mon Sep 17 00:00:00 2001 From: Mohamed-Hacene Date: Fri, 17 Jan 2025 14:38:36 +0100 Subject: [PATCH] feat: improve error handling --- backend/core/views.py | 45 ++++++++++--------- frontend/messages/en.json | 4 +- frontend/messages/fr.json | 4 +- .../[model=urlmodel]/+page.server.ts | 35 +++++++++++++-- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/backend/core/views.py b/backend/core/views.py index a5ab8af71..7d70db60d 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -2051,7 +2051,9 @@ def _process_uploaded_file(self, dump_file): logger.error("Invalid JSON format in uploaded file", exc_info=e) raise if not import_version == VERSION: - logger.error(f"Import version {import_version} not compatible with current version {VERSION}") + logger.error( + f"Import version {import_version} not compatible with current version {VERSION}" + ) raise ValidationError( {"file": "importVersionNotCompatibleWithCurrentVersion"} ) @@ -2084,17 +2086,18 @@ def _import_objects(self, parsed_data, domain_name: str): try: objects = parsed_data.get("objects", None) if not objects: - return {"message": "No objects to import"} + logger.error("No objects found in the dump") + raise ValidationError({"error": "No objects found in the dump"}) # Validate models and check for domain models_map = self._get_models_map(objects) if Folder in models_map.values(): logger.error("Dump contains a domain") - return {"error": "Dump contains a domain"} + raise ValidationError({"error": "Dump contains a domain"}) # Validation phase (outside transaction since it doesn't modify database) creation_order = self._resolve_dependencies(list(models_map.values())) - + for model in creation_order: self._validate_model_objects( model=model, @@ -2105,17 +2108,13 @@ def _import_objects(self, parsed_data, domain_name: str): if validation_errors: logger.error(f"Validation errors: {validation_errors}") - return {"validation_errors": validation_errors} + raise ValidationError({"validation error"}) # Check for missing libraries for library in required_libraries: - if not LoadedLibrary.objects.filter(urn=library["urn"]).exists(): + if not LoadedLibrary.objects.filter(urn=library).exists(): missing_libraries.append(library) - if missing_libraries: - logger.warning(f"Missing libraries: {missing_libraries}") - return {"missing_libraries": missing_libraries} - # Creation phase - wrap in transaction with transaction.atomic(): # Create base folder and store its ID @@ -2135,9 +2134,10 @@ def _import_objects(self, parsed_data, domain_name: str): return {"message": "Import successful"} except ValidationError as e: + if missing_libraries == "missingLibraries": + logger.warning(f"Missing libraries: {missing_libraries}") + raise ValidationError({"missing_libraries": missing_libraries}) logger.exception(f"Failed to import objects: {str(e)}") - # The transaction.atomic() context manager will automatically - # roll back all changes if an exception occurs raise ValidationError({"non_field_errors": "errorOccuredDuringImport"}) def _validate_model_objects( @@ -2172,9 +2172,7 @@ def _validate_batch(self, model, batch, validation_errors, required_libraries): # Handle library objects if fields.get("library") or model == LoadedLibrary: if model == LoadedLibrary: - required_libraries.append( - {"urn": fields["urn"], "name": fields["name"]} - ) + required_libraries.append(fields["urn"]) continue # Validate using serializer @@ -2314,12 +2312,12 @@ def get_mapped_ids( urn=fields.get("risk_matrix") ) fields["ebios_rm_study"] = ( - EbiosRMStudy.objects.get( - id=link_dump_database_ids.get(fields["ebios_rm_study"]) - ) - if fields.get("ebios_rm_study") - else None + EbiosRMStudy.objects.get( + id=link_dump_database_ids.get(fields["ebios_rm_study"]) ) + if fields.get("ebios_rm_study") + else None + ) case "complianceassessment": fields["project"] = Project.objects.get( @@ -2407,7 +2405,8 @@ def get_mapped_ids( fields.pop("assets", []), link_dump_database_ids ), "compliance_assessment_ids": get_mapped_ids( - fields.pop("compliance_assessments", []), link_dump_database_ids + fields.pop("compliance_assessments", []), + link_dump_database_ids, ), } ) @@ -2576,7 +2575,9 @@ def _set_many_to_many_relations(self, model, obj, many_to_many_map_ids): case "attackpath": if stakeholder_ids := many_to_many_map_ids.get("stakeholder_ids"): - obj.stakeholders.set(Stakeholder.objects.filter(id__in=stakeholder_ids)) + obj.stakeholders.set( + Stakeholder.objects.filter(id__in=stakeholder_ids) + ) case "operationalscenario": if threat_ids := many_to_many_map_ids.get("threat_ids"): diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b67ebfe22..29d10f325 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1091,5 +1091,7 @@ "importFolder": "Import domain", "importFolderHelpText": "Import a domain from a JSON or ZIP file", "importVersionNotCompatibleWithCurrentVersion": "The import version is not compatible with the current version of the application", - "errorOccuredDuringImport": "An error occurred during the import" + "errorOccuredDuringImport": "An error occurred during the import", + "successfullyImportedFolder": "Folder successfully imported", + "missingLibrariesInImport": "Some libraries are missing, see the list above" } diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index cc1f71ae9..247401b1e 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -1091,5 +1091,7 @@ "importFolder": "Importer un domaine", "importFolderHelpText": "Importer un domaine à partir d'un ZIP ou fichier JSON", "importVersionNotCompatibleWithCurrentVersion": "La version de l'import n'est pas compatible avec la version actuelle de l'application.", - "errorOccuredDuringImport": "Une erreur s'est produite lors de l'import" + "errorOccuredDuringImport": "Une erreur s'est produite lors de l'import", + "successfullyImportedFolder": "Domaine importé avec succès", + "missingLibrariesInImport": "Certaines bibliothèques sont manquantes, voir la liste ci-dessus" } diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts index 5ff92e535..f40f7448d 100644 --- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts +++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts @@ -1,4 +1,4 @@ -import { defaultDeleteFormAction, defaultWriteFormAction, handleErrorResponse } from '$lib/utils/actions'; +import { defaultDeleteFormAction, defaultWriteFormAction } from '$lib/utils/actions'; import { BASE_API_URL } from '$lib/utils/constants'; import { getModelInfo, @@ -8,10 +8,13 @@ import { import { modelSchema } from '$lib/utils/schemas'; import type { ModelInfo } from '$lib/utils/types'; import { type Actions } from '@sveltejs/kit'; -import { fail, superValidate, withFiles } from 'sveltekit-superforms'; +import { fail, superValidate, withFiles, setError } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import { z } from 'zod'; import type { PageServerLoad } from './$types'; +import { setFlash } from 'sveltekit-flash-message/server'; +import * as m from '$paraglide/messages'; +import { safeTranslate } from '$lib/utils/i18n'; export const load: PageServerLoad = async ({ params, fetch }) => { const schema = z.object({ id: z.string().uuid() }); @@ -108,8 +111,34 @@ export const actions: Actions = { }, body: file }); + const res = await response.json(); - if (!response.ok) return await handleErrorResponse({ event, response: response, form }); + if (!response.ok && res.missing_libraries) { + setError(form, 'file', m.missingLibrariesInImport()); + for (const value of res.missing_libraries) { + setError(form, 'non_field_errors', value); + } + return fail(400, { form }); + } + + if (!response.ok) { + if (res.error) { + setFlash({ type: 'error', message: safeTranslate(res.error) }, event); + return { form }; + } + Object.entries(res).forEach(([key, value]) => { + setError(form, key, safeTranslate(value)); + }); + return fail(400, { form }); + } + + setFlash( + { + type: 'success', + message: m.successfullyImportedFolder() + }, + event + ); return withFiles({ form }); }