Skip to content

Commit 80ab025

Browse files
feat(UX): Improve some custom library upload error messages (#3318)
* feat(UX): Improve some custom library upload error messages * Fix tests * Add load method error support * Update views.py * fix type annotations, adapt all calls --------- Co-authored-by: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
1 parent 6382ddb commit 80ab025

4 files changed

Lines changed: 49 additions & 28 deletions

File tree

backend/core/models.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import hashlib
55
from datetime import date, datetime
66
from pathlib import Path
7-
from typing import Self, Union, List, Optional, Literal
7+
from typing import Self, Union, List, Optional, Literal, Tuple
88
import statistics
99

1010
from django.contrib.contenttypes.models import ContentType
@@ -306,11 +306,11 @@ def __init_class__(cls):
306306
@classmethod
307307
def store_library_content(
308308
cls, library_content: bytes, builtin: bool = False, dry_run: bool = False
309-
) -> Union["StoredLibrary", dict, None]:
309+
) -> Tuple[Optional[Union["StoredLibrary", dict]], Optional[str]]:
310310
hash_checksum = sha256(library_content)
311311
if not dry_run and hash_checksum in StoredLibrary.HASH_CHECKSUM_SET:
312312
# We do not store the library if its hash checksum is in the database.
313-
return None
313+
return None, "libraryAlreadyLoadedError"
314314
try:
315315
library_data = yaml.safe_load(library_content)
316316
if not isinstance(library_data, dict):
@@ -344,21 +344,24 @@ def store_library_content(
344344
key: (1 if key == "framework" else len(value))
345345
for key, value in library_data["objects"].items()
346346
}
347-
return {
348-
"name": library_data["name"],
349-
"urn": urn,
350-
"locale": locale,
351-
"version": version,
352-
"ref_id": library_data.get("ref_id"),
353-
"description": library_data.get("description"),
354-
"provider": library_data.get("provider"),
355-
"packager": library_data.get("packager"),
356-
"publication_date": library_data.get("publication_date"),
357-
"objects_meta": objects_meta,
358-
"is_loaded": is_loaded,
359-
"builtin": builtin,
360-
"copyright": library_data.get("copyright"),
361-
}
347+
return (
348+
{
349+
"name": library_data["name"],
350+
"urn": urn,
351+
"locale": locale,
352+
"version": version,
353+
"ref_id": library_data.get("ref_id"),
354+
"description": library_data.get("description"),
355+
"provider": library_data.get("provider"),
356+
"packager": library_data.get("packager"),
357+
"publication_date": library_data.get("publication_date"),
358+
"objects_meta": objects_meta,
359+
"is_loaded": is_loaded,
360+
"builtin": builtin,
361+
"copyright": library_data.get("copyright"),
362+
},
363+
None,
364+
)
362365

363366
same_version_lib = StoredLibrary.objects.filter(
364367
urn=urn, locale=locale, version=version
@@ -368,10 +371,13 @@ def store_library_content(
368371
logger.info("update hash", urn=urn)
369372
same_version_lib.hash_checksum = hash_checksum
370373
same_version_lib.save()
371-
return None
374+
return None, "libraryAlreadyLoadedError"
372375

373376
if StoredLibrary.objects.filter(urn=urn, locale=locale, version__gte=version):
374-
return None # We do not accept to store outdated libraries
377+
return (
378+
None,
379+
"libraryOutdatedError",
380+
) # We do not accept to store outdated libraries
375381

376382
with transaction.atomic():
377383
# This code allows adding outdated libraries in the library store but they will be erased if a greater version of this library is stored.
@@ -425,16 +431,16 @@ def store_library_content(
425431
), # autoload is true if the library contains requirement mapping sets
426432
)
427433
new_library.filtering_labels.set(filtering_labels)
428-
return new_library
434+
return new_library, None
429435

430436
@classmethod
431437
def store_library_file(
432438
cls, fname: Path, builtin: bool = False
433-
) -> "StoredLibrary | None":
439+
) -> Tuple[Optional["StoredLibrary"], Optional[str]]:
434440
with open(fname, "rb") as f:
435441
library_content = f.read()
436442

437-
return StoredLibrary.store_library_content(library_content, builtin)
443+
return StoredLibrary.store_library_content(library_content, builtin=builtin)
438444

439445
def get_loaded_library(self) -> Optional["LoadedLibrary"]:
440446
if not self.is_loaded:

backend/library/management/commands/storelibraries.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ def handle(self, *args, **options):
3131
for fname in library_files:
3232
# logger.info("Begin library file storage", filename=fname)
3333
try:
34-
library = StoredLibrary.store_library_file(fname, True)
34+
library, error = StoredLibrary.store_library_file(fname, True)
3535
if library:
3636
logger.info(
3737
"Successfully stored library",
3838
filename=fname,
3939
library=library,
4040
)
41+
elif error:
42+
logger.warning(
43+
"Library skipped",
44+
filename=fname,
45+
reason=error,
46+
)
4147
except Exception:
4248
logger.error("Invalid library file", filename=fname)
4349

backend/library/tests/test_store_library_content.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,25 +140,26 @@
140140
class TestStoreLibraryContent:
141141
@pytest.mark.django_db
142142
def test_store_library_content_sets_autoload_to_true_if_lib_has_mappings(self):
143-
stored_library = StoredLibrary.store_library_content(
143+
stored_library, _ = StoredLibrary.store_library_content(
144144
SAMPLE_YAML_LIB_WITH_MAPPINGS
145145
)
146146
assert stored_library is not None
147147
assert stored_library.autoload is True
148148

149149
@pytest.mark.django_db
150150
def test_store_library_content_sets_autoload_to_false_if_lib_has_no_mappings(self):
151-
stored_library = StoredLibrary.store_library_content(
151+
stored_library, _ = StoredLibrary.store_library_content(
152152
SAMPLE_YAML_LIB_NO_MAPPINGS
153153
)
154154
assert stored_library is not None
155155
assert stored_library.autoload is False
156156

157157
@pytest.mark.django_db
158158
def test_store_library_content_dry_run(self):
159-
library_data = StoredLibrary.store_library_content(
159+
library_data, error = StoredLibrary.store_library_content(
160160
SAMPLE_YAML_LIB_NO_MAPPINGS, dry_run=True
161161
)
162+
assert error is None
162163
assert isinstance(library_data, dict)
163164
assert library_data["urn"] == "urn:intuitem:test:library:nist-csf-1.1"
164165
assert library_data["version"] == 5

backend/library/views.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,15 @@ def upload_library(self, request):
446446
dry_run = request.query_params.get("dry_run", "false").lower() == "true"
447447

448448
# Store the library content (YAML bytes)
449-
library = StoredLibrary.store_library_content(content, dry_run=dry_run)
449+
library, error = StoredLibrary.store_library_content(
450+
content, dry_run=dry_run
451+
)
452+
if error is not None:
453+
return HttpResponse(
454+
json.dumps({"error": error}),
455+
status=HTTP_422_UNPROCESSABLE_ENTITY,
456+
)
457+
450458
if dry_run:
451459
logger.info("Dry run library upload successful")
452460
return Response(library)

0 commit comments

Comments
 (0)