From fcdf6567420aa1a0c64a6f4427ec8841743cdef8 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Fri, 4 Jul 2025 13:28:52 -0600 Subject: [PATCH 1/3] feat: lp_dump management command added --- .importlinter | 3 ++ .../apps/authoring/backup_restore/__init__.py | 0 .../apps/authoring/backup_restore/admin.py | 3 ++ .../apps/authoring/backup_restore/api.py | 19 ++++++++ .../apps/authoring/backup_restore/apps.py | 12 +++++ .../authoring/backup_restore/constants.py | 5 ++ .../backup_restore/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/lp_dump.py | 29 ++++++++++++ .../backup_restore/migrations/__init__.py | 0 .../apps/authoring/backup_restore/models.py | 3 ++ .../apps/authoring/backup_restore/utils.py | 46 +++++++++++++++++++ projects/dev.py | 1 + 13 files changed, 121 insertions(+) create mode 100644 openedx_learning/apps/authoring/backup_restore/__init__.py create mode 100644 openedx_learning/apps/authoring/backup_restore/admin.py create mode 100644 openedx_learning/apps/authoring/backup_restore/api.py create mode 100644 openedx_learning/apps/authoring/backup_restore/apps.py create mode 100644 openedx_learning/apps/authoring/backup_restore/constants.py create mode 100644 openedx_learning/apps/authoring/backup_restore/management/__init__.py create mode 100644 openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py create mode 100644 openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py create mode 100644 openedx_learning/apps/authoring/backup_restore/migrations/__init__.py create mode 100644 openedx_learning/apps/authoring/backup_restore/models.py create mode 100644 openedx_learning/apps/authoring/backup_restore/utils.py diff --git a/.importlinter b/.importlinter index a0c2ba7da..0317245a6 100644 --- a/.importlinter +++ b/.importlinter @@ -50,6 +50,9 @@ layers= # Its only dependency should be the publishing app. openedx_learning.apps.authoring.collections + # The "backup_restore" app handle the new export and import mechanism. + openedx_learning.apps.authoring.backup_restore + # The lowest layer is "publishing", which holds the basic primitives needed # to create Learning Packages and manage the draft and publish states for # various types of content. diff --git a/openedx_learning/apps/authoring/backup_restore/__init__.py b/openedx_learning/apps/authoring/backup_restore/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openedx_learning/apps/authoring/backup_restore/admin.py b/openedx_learning/apps/authoring/backup_restore/admin.py new file mode 100644 index 000000000..5c6b7e6de --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/admin.py @@ -0,0 +1,3 @@ +""" +Django Admin pages for Backup Restore models (WIP) +""" diff --git a/openedx_learning/apps/authoring/backup_restore/api.py b/openedx_learning/apps/authoring/backup_restore/api.py new file mode 100644 index 000000000..f349f9e88 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -0,0 +1,19 @@ +""" +Backup Restore API +""" +import zipfile + +from openedx_learning.apps.authoring.backup_restore.constants import TOML_PACKAGE_NAME +from openedx_learning.apps.authoring.backup_restore.utils import LpTomlFile + + +def create_zip_file(lp_key: str, path: str) -> None: + """ + Creates a zip file with a toml file so far (WIP) + """ + toml_file = LpTomlFile() + toml_file.create(lp_key) + 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 + zipf.writestr(TOML_PACKAGE_NAME, toml_content) diff --git a/openedx_learning/apps/authoring/backup_restore/apps.py b/openedx_learning/apps/authoring/backup_restore/apps.py new file mode 100644 index 000000000..a7536bed7 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/apps.py @@ -0,0 +1,12 @@ +""" +Unit Django application initialization. +""" + +from django.apps import AppConfig + + +class BackupRestoreConfig(AppConfig): + name = 'openedx_learning.apps.authoring.backup_restore' + verbose_name = "Learning Core > Authoring > Backup Restore" + default_auto_field = 'django.db.models.BigAutoField' + label = "oel_backup_restore" diff --git a/openedx_learning/apps/authoring/backup_restore/constants.py b/openedx_learning/apps/authoring/backup_restore/constants.py new file mode 100644 index 000000000..8cc721b7d --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/constants.py @@ -0,0 +1,5 @@ +""" +Contstant for backup restore app +""" + +TOML_PACKAGE_NAME = "package.toml" diff --git a/openedx_learning/apps/authoring/backup_restore/management/__init__.py b/openedx_learning/apps/authoring/backup_restore/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py b/openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e9d076f43 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py @@ -0,0 +1,29 @@ +""" +Django management commands to handle backup and restore learning packages (WIP) +""" + +from django.core.management.base import BaseCommand + +from openedx_learning.apps.authoring.backup_restore.api import create_zip_file + + +class Command(BaseCommand): + """ + Django management command to export a learning package on a zip file + """ + help = 'Export a learning package on a zip file' + + def add_arguments(self, parser): + parser.add_argument('lp_key', type=str, help='The key of the LearningPackage to dump') + parser.add_argument('file_name', type=str, help='The name of the output zip file') + + def handle(self, *args, **options): + lp_key = options['lp_key'] + file_name = options['file_name'] + try: + create_zip_file(lp_key, file_name) + message = f'The [{file_name}] was created with [{lp_key}] learning package key' + self.stdout.write(self.style.SUCCESS(message)) + except Exception as e: # pylint: disable=broad-exception-caught + message = f"Error on create the zip file {e}" + self.stdout.write(self.style.ERROR(message)) diff --git a/openedx_learning/apps/authoring/backup_restore/migrations/__init__.py b/openedx_learning/apps/authoring/backup_restore/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openedx_learning/apps/authoring/backup_restore/models.py b/openedx_learning/apps/authoring/backup_restore/models.py new file mode 100644 index 000000000..87318c697 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/models.py @@ -0,0 +1,3 @@ +""" +Core models for Backup Restore (WIP) +""" diff --git a/openedx_learning/apps/authoring/backup_restore/utils.py b/openedx_learning/apps/authoring/backup_restore/utils.py new file mode 100644 index 000000000..b21831529 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/utils.py @@ -0,0 +1,46 @@ +""" +Utilities for backup and restore app +""" + +from datetime import datetime +from typing import Any, Dict + +from tomlkit import comment, document, dumps, nl, table +from tomlkit.items import Table + + +class LpTomlFile(): + """ + Class to create a .toml file of a learning package (WIP) + """ + + def __init__(self): + self.doc = document() + + def _create_header(self) -> None: + self.doc.add(comment(f"Datetime of the export: {datetime.now()}")) + self.doc.add(nl()) + self.doc.add("title", "Learning package example") + + def _create_table(self, params: Dict[str, Any]) -> Table: + section = table() + for key, value in params.items(): + section.add(key, value) + return section + + def create(self, lp_key: str) -> None: + """ + Process the toml file + """ + self._create_header() + section = self._create_table({ + "title": "", + "key": lp_key, + "description": "", + "created": "", + "updated": "" + }) + self.doc.add("Learning package", section) + + def get(self) -> str: + return dumps(self.doc) diff --git a/projects/dev.py b/projects/dev.py index 41bd7ec58..1b3b42f47 100644 --- a/projects/dev.py +++ b/projects/dev.py @@ -38,6 +38,7 @@ "openedx_learning.apps.authoring.sections.apps.SectionsConfig", "openedx_learning.apps.authoring.subsections.apps.SubsectionsConfig", "openedx_learning.apps.authoring.units.apps.UnitsConfig", + "openedx_learning.apps.authoring.backup_restore.apps.BackupRestoreConfig", # Learning Contrib Apps "openedx_learning.contrib.media_server.apps.MediaServerConfig", # Apps that don't belong in this repo in the long term, but are here to make From 1dc040fca58d8ace7bb0c3c2ca87c074396223c7 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Mon, 7 Jul 2025 11:50:23 -0600 Subject: [PATCH 2/3] chore: Add tomlkit to project requirements --- requirements/base.in | 2 ++ requirements/base.txt | 2 ++ requirements/dev.txt | 10 ---------- requirements/doc.txt | 2 ++ requirements/quality.txt | 11 +++-------- requirements/test.txt | 2 ++ 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index f439bd117..15bcd9748 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -11,3 +11,5 @@ djangorestframework<4.0 # REST API edx-drf-extensions # Extensions to the Django REST Framework used by Open edX rules<4.0 # Django extension for rules-based authorization checks + +tomlkit # Parses and writes TOML configuration files diff --git a/requirements/base.txt b/requirements/base.txt index 95998225e..bbe998683 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -106,6 +106,8 @@ stevedore==5.4.1 # via # edx-django-utils # edx-opaque-keys +tomlkit==0.13.3 + # via -r requirements/base.in typing-extensions==4.14.1 # via edx-opaque-keys tzdata==2025.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index ded1e5dd4..e0891e10a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -102,7 +102,6 @@ cryptography==45.0.5 # via # -r requirements/quality.txt # pyjwt - # secretstorage ddt==1.7.2 # via -r requirements/quality.txt diff-cover==9.4.1 @@ -231,11 +230,6 @@ jaraco-functools==4.2.1 # via # -r requirements/quality.txt # keyring -jeepney==0.9.0 - # via - # -r requirements/quality.txt - # keyring - # secretstorage jinja2==3.1.6 # via # -r requirements/quality.txt @@ -449,10 +443,6 @@ rich==14.0.0 # twine rules==3.5 # via -r requirements/quality.txt -secretstorage==3.3.3 - # via - # -r requirements/quality.txt - # keyring semantic-version==2.10.0 # via # -r requirements/quality.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 5a4741fff..8c434fca3 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -335,6 +335,8 @@ text-unidecode==1.3 # via # -r requirements/test.txt # python-slugify +tomlkit==0.13.3 + # via -r requirements/test.txt types-pyyaml==6.0.12.20250516 # via # -r requirements/test.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 8981ae25a..e3fd3861b 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -77,7 +77,6 @@ cryptography==45.0.5 # via # -r requirements/test.txt # pyjwt - # secretstorage ddt==1.7.2 # via -r requirements/test.txt dill==0.4.0 @@ -173,10 +172,6 @@ jaraco-context==6.0.1 # via keyring jaraco-functools==4.2.1 # via keyring -jeepney==0.9.0 - # via - # keyring - # secretstorage jinja2==3.1.6 # via # -r requirements/test.txt @@ -327,8 +322,6 @@ rich==14.0.0 # via twine rules==3.5 # via -r requirements/test.txt -secretstorage==3.3.3 - # via keyring semantic-version==2.10.0 # via # -r requirements/test.txt @@ -356,7 +349,9 @@ text-unidecode==1.3 # -r requirements/test.txt # python-slugify tomlkit==0.13.3 - # via pylint + # via + # -r requirements/test.txt + # pylint twine==6.1.0 # via -r requirements/quality.in types-pyyaml==6.0.12.20250516 diff --git a/requirements/test.txt b/requirements/test.txt index 9a1bca5c9..337b1da13 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -239,6 +239,8 @@ stevedore==5.4.1 # edx-opaque-keys text-unidecode==1.3 # via python-slugify +tomlkit==0.13.3 + # via -r requirements/base.txt types-pyyaml==6.0.12.20250516 # via # django-stubs From 0cc1b64a218aa0aad45e80c1bdb2c900686255e5 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Mon, 7 Jul 2025 13:23:09 -0600 Subject: [PATCH 3/3] fix: address code review comments --- .../apps/authoring/backup_restore/api.py | 7 ++++--- .../apps/authoring/backup_restore/apps.py | 2 +- .../apps/authoring/backup_restore/constants.py | 5 ----- .../management/commands/lp_dump.py | 18 +++++++++++++----- .../backup_restore/{utils.py => toml.py} | 5 ++--- 5 files changed, 20 insertions(+), 17 deletions(-) delete mode 100644 openedx_learning/apps/authoring/backup_restore/constants.py rename openedx_learning/apps/authoring/backup_restore/{utils.py => toml.py} (88%) diff --git a/openedx_learning/apps/authoring/backup_restore/api.py b/openedx_learning/apps/authoring/backup_restore/api.py index f349f9e88..43527c77b 100644 --- a/openedx_learning/apps/authoring/backup_restore/api.py +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -3,15 +3,16 @@ """ import zipfile -from openedx_learning.apps.authoring.backup_restore.constants import TOML_PACKAGE_NAME -from openedx_learning.apps.authoring.backup_restore.utils import LpTomlFile +from .toml import TOMLLearningPackageFile + +TOML_PACKAGE_NAME = "package.toml" def create_zip_file(lp_key: str, path: str) -> None: """ Creates a zip file with a toml file so far (WIP) """ - toml_file = LpTomlFile() + toml_file = TOMLLearningPackageFile() toml_file.create(lp_key) toml_content: str = toml_file.get() with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: diff --git a/openedx_learning/apps/authoring/backup_restore/apps.py b/openedx_learning/apps/authoring/backup_restore/apps.py index a7536bed7..7aa3f022b 100644 --- a/openedx_learning/apps/authoring/backup_restore/apps.py +++ b/openedx_learning/apps/authoring/backup_restore/apps.py @@ -1,5 +1,5 @@ """ -Unit Django application initialization. +Backup/Restore application initialization. """ from django.apps import AppConfig diff --git a/openedx_learning/apps/authoring/backup_restore/constants.py b/openedx_learning/apps/authoring/backup_restore/constants.py deleted file mode 100644 index 8cc721b7d..000000000 --- a/openedx_learning/apps/authoring/backup_restore/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Contstant for backup restore app -""" - -TOML_PACKAGE_NAME = "package.toml" 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 e9d076f43..fa0de8579 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 @@ -1,17 +1,20 @@ """ Django management commands to handle backup and restore learning packages (WIP) """ +import logging from django.core.management.base import BaseCommand from openedx_learning.apps.authoring.backup_restore.api import create_zip_file +logger = logging.getLogger(__name__) + class Command(BaseCommand): """ - Django management command to export a learning package on a zip file + Django management command to export a learning package to a zip file. """ - help = 'Export a learning package on a zip file' + help = 'Export a learning package to a zip file.' def add_arguments(self, parser): parser.add_argument('lp_key', type=str, help='The key of the LearningPackage to dump') @@ -22,8 +25,13 @@ def handle(self, *args, **options): file_name = options['file_name'] try: create_zip_file(lp_key, file_name) - message = f'The [{file_name}] was created with [{lp_key}] learning package key' + message = f'{lp_key} written to {file_name}' self.stdout.write(self.style.SUCCESS(message)) except Exception as e: # pylint: disable=broad-exception-caught - message = f"Error on create the zip file {e}" - self.stdout.write(self.style.ERROR(message)) + message = f"Error creating zip file: error {e}" + self.stderr.write(self.style.ERROR(message)) + logger.exception( + "Failed to create zip file %s (learning‑package key %s)", + file_name, + lp_key, + ) diff --git a/openedx_learning/apps/authoring/backup_restore/utils.py b/openedx_learning/apps/authoring/backup_restore/toml.py similarity index 88% rename from openedx_learning/apps/authoring/backup_restore/utils.py rename to openedx_learning/apps/authoring/backup_restore/toml.py index b21831529..01ae71cc5 100644 --- a/openedx_learning/apps/authoring/backup_restore/utils.py +++ b/openedx_learning/apps/authoring/backup_restore/toml.py @@ -9,7 +9,7 @@ from tomlkit.items import Table -class LpTomlFile(): +class TOMLLearningPackageFile(): """ Class to create a .toml file of a learning package (WIP) """ @@ -20,7 +20,6 @@ def __init__(self): def _create_header(self) -> None: self.doc.add(comment(f"Datetime of the export: {datetime.now()}")) self.doc.add(nl()) - self.doc.add("title", "Learning package example") def _create_table(self, params: Dict[str, Any]) -> Table: section = table() @@ -40,7 +39,7 @@ def create(self, lp_key: str) -> None: "created": "", "updated": "" }) - self.doc.add("Learning package", section) + self.doc.add("learning_package", section) def get(self) -> str: return dumps(self.doc)