From 000c9fae44d72af038b2889fda6798380294120e Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Tue, 8 Jul 2025 16:07:06 -0600 Subject: [PATCH 1/3] feat: export LearningPackage data in lp_dump command Updates the lp_dump management command to export real data from the LearningPackage model instead of using stub values --- .../apps/authoring/backup_restore/api.py | 9 +++++++-- .../management/commands/lp_dump.py | 4 ++++ .../apps/authoring/backup_restore/toml.py | 17 ++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/openedx_learning/apps/authoring/backup_restore/api.py b/openedx_learning/apps/authoring/backup_restore/api.py index 43527c77b..b26fc207a 100644 --- a/openedx_learning/apps/authoring/backup_restore/api.py +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -3,6 +3,8 @@ """ import zipfile +from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key + from .toml import TOMLLearningPackageFile TOML_PACKAGE_NAME = "package.toml" @@ -11,9 +13,12 @@ def create_zip_file(lp_key: str, path: str) -> None: """ Creates a zip file with a toml file so far (WIP) + + Can throw a NotFoundError at get_learning_package_by_key """ - toml_file = TOMLLearningPackageFile() - toml_file.create(lp_key) + learning_package = get_learning_package_by_key(lp_key) + toml_file = TOMLLearningPackageFile(learning_package) + toml_file.create() toml_content: str = toml_file.get() with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: # Add the TOML string as a file in the ZIP diff --git a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py index fa0de8579..5691aede4 100644 --- a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +++ b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py @@ -3,6 +3,7 @@ """ import logging +from django.core.exceptions import ObjectDoesNotExist from django.core.management.base import BaseCommand from openedx_learning.apps.authoring.backup_restore.api import create_zip_file @@ -27,6 +28,9 @@ def handle(self, *args, **options): create_zip_file(lp_key, file_name) message = f'{lp_key} written to {file_name}' self.stdout.write(self.style.SUCCESS(message)) + except ObjectDoesNotExist: + message = f"Learning package with key {lp_key} not found" + self.stderr.write(self.style.ERROR(message)) except Exception as e: # pylint: disable=broad-exception-caught message = f"Error creating zip file: error {e}" self.stderr.write(self.style.ERROR(message)) diff --git a/openedx_learning/apps/authoring/backup_restore/toml.py b/openedx_learning/apps/authoring/backup_restore/toml.py index 01ae71cc5..69af68bf0 100644 --- a/openedx_learning/apps/authoring/backup_restore/toml.py +++ b/openedx_learning/apps/authoring/backup_restore/toml.py @@ -8,14 +8,17 @@ from tomlkit import comment, document, dumps, nl, table from tomlkit.items import Table +from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage + class TOMLLearningPackageFile(): """ Class to create a .toml file of a learning package (WIP) """ - def __init__(self): + def __init__(self, learning_package: LearningPackage): self.doc = document() + self.learning_package = learning_package def _create_header(self) -> None: self.doc.add(comment(f"Datetime of the export: {datetime.now()}")) @@ -27,17 +30,17 @@ def _create_table(self, params: Dict[str, Any]) -> Table: section.add(key, value) return section - def create(self, lp_key: str) -> None: + def create(self) -> None: """ Process the toml file """ self._create_header() section = self._create_table({ - "title": "", - "key": lp_key, - "description": "", - "created": "", - "updated": "" + "title": self.learning_package.title, + "key": self.learning_package.key, + "description": self.learning_package.description, + "created": self.learning_package.created, + "updated": self.learning_package.updated }) self.doc.add("learning_package", section) From 27bfeca68c4ebcc13b7051ffe2aeea5f6203c5be Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Wed, 9 Jul 2025 13:20:08 -0600 Subject: [PATCH 2/3] feat: add docstrings and raise CommandError in lp_dump command --- .../management/commands/lp_dump.py | 7 +++-- .../apps/authoring/backup_restore/toml.py | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py index 5691aede4..f26b1f244 100644 --- a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +++ b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py @@ -3,10 +3,11 @@ """ import logging -from django.core.exceptions import ObjectDoesNotExist +from django.core.management import CommandError from django.core.management.base import BaseCommand from openedx_learning.apps.authoring.backup_restore.api import create_zip_file +from openedx_learning.apps.authoring.publishing.api import LearningPackage logger = logging.getLogger(__name__) @@ -28,9 +29,9 @@ def handle(self, *args, **options): create_zip_file(lp_key, file_name) message = f'{lp_key} written to {file_name}' self.stdout.write(self.style.SUCCESS(message)) - except ObjectDoesNotExist: + except LearningPackage.DoesNotExist as exc: message = f"Learning package with key {lp_key} not found" - self.stderr.write(self.style.ERROR(message)) + raise CommandError(message) from exc except Exception as e: # pylint: disable=broad-exception-caught message = f"Error creating zip file: error {e}" self.stderr.write(self.style.ERROR(message)) diff --git a/openedx_learning/apps/authoring/backup_restore/toml.py b/openedx_learning/apps/authoring/backup_restore/toml.py index 69af68bf0..fd3854204 100644 --- a/openedx_learning/apps/authoring/backup_restore/toml.py +++ b/openedx_learning/apps/authoring/backup_restore/toml.py @@ -13,7 +13,10 @@ class TOMLLearningPackageFile(): """ - Class to create a .toml file of a learning package (WIP) + Class to create a .toml representation of a LearningPackage instance. + + This class builds a structured TOML document using `tomlkit` with metadata and fields + extracted from a `LearningPackage` object. The output can later be saved to a file or used elsewhere. """ def __init__(self, learning_package: LearningPackage): @@ -21,10 +24,23 @@ def __init__(self, learning_package: LearningPackage): self.learning_package = learning_package def _create_header(self) -> None: + """ + Adds a comment with the current datetime to indicate when the export occurred. + This helps with traceability and file versioning. + """ self.doc.add(comment(f"Datetime of the export: {datetime.now()}")) self.doc.add(nl()) def _create_table(self, params: Dict[str, Any]) -> Table: + """ + Builds a TOML table section from a dictionary of key-value pairs. + + Args: + params (Dict[str, Any]): A dictionary containing keys and values to include in the TOML table. + + Returns: + Table: A TOML table populated with the provided keys and values. + """ section = table() for key, value in params.items(): section.add(key, value) @@ -32,7 +48,10 @@ def _create_table(self, params: Dict[str, Any]) -> Table: def create(self) -> None: """ - Process the toml file + Populates the TOML document with a header and a table containing + metadata from the LearningPackage instance. + + This method must be called before calling `get()`, otherwise the document will be empty. """ self._create_header() section = self._create_table({ @@ -45,4 +64,9 @@ def create(self) -> None: self.doc.add("learning_package", section) def get(self) -> str: + """ + Returns: + str: The string representation of the generated TOML document. + Ensure `create()` has been called beforehand to get meaningful output. + """ return dumps(self.doc) From 0436b6965ca32e92ab697afddc777125523e9d2a Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Wed, 9 Jul 2025 15:31:23 -0600 Subject: [PATCH 3/3] feat: apply improvements to lp_dump command structure and output --- .../backup_restore/management/commands/lp_dump.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py index f26b1f244..12acf2f04 100644 --- a/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +++ b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py @@ -25,18 +25,21 @@ def add_arguments(self, parser): def handle(self, *args, **options): lp_key = options['lp_key'] file_name = options['file_name'] + if not file_name.endswith(".zip"): + raise CommandError("Output file name must end with .zip") try: create_zip_file(lp_key, file_name) message = f'{lp_key} written to {file_name}' self.stdout.write(self.style.SUCCESS(message)) except LearningPackage.DoesNotExist as exc: message = f"Learning package with key {lp_key} not found" + logger.exception(message) raise CommandError(message) from exc - except Exception as e: # pylint: disable=broad-exception-caught - message = f"Error creating zip file: error {e}" - self.stderr.write(self.style.ERROR(message)) + except Exception as e: + message = f"Failed to export learning package '{lp_key}': {e}" logger.exception( "Failed to create zip file %s (learning‑package key %s)", file_name, lp_key, ) + raise CommandError(message) from e