From a6c2f41a87cd978348d69e5310cae12364664485 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Fri, 7 Nov 2025 17:25:04 -0600 Subject: [PATCH 1/2] feat: include origin server metadata in backup archive library --- openedx_learning/__init__.py | 2 +- .../apps/authoring/backup_restore/api.py | 4 +-- .../management/commands/lp_dump.py | 9 +++++- .../apps/authoring/backup_restore/zipper.py | 28 +++++++++++++++---- .../authoring/backup_restore/test_backup.py | 12 +++++--- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 60b0a4af..12091801 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -2,4 +2,4 @@ Open edX Learning ("Learning Core"). """ -__version__ = "0.30.0" +__version__ = "0.30.1" diff --git a/openedx_learning/apps/authoring/backup_restore/api.py b/openedx_learning/apps/authoring/backup_restore/api.py index 2324e6e0..0a60fcc8 100644 --- a/openedx_learning/apps/authoring/backup_restore/api.py +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -9,7 +9,7 @@ from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key -def create_zip_file(lp_key: str, path: str, user: UserType | None = None) -> None: +def create_zip_file(lp_key: str, path: str, user: UserType | None = None, origin_server: str | None = None) -> None: """ Creates a dump zip file for the given learning package key at the given path. The zip file contains a TOML representation of the learning package and its contents. @@ -17,7 +17,7 @@ def create_zip_file(lp_key: str, path: str, user: UserType | None = None) -> Non Can throw a NotFoundError at get_learning_package_by_key """ learning_package = get_learning_package_by_key(lp_key) - LearningPackageZipper(learning_package, user).create_zip(path) + LearningPackageZipper(learning_package, path, user, origin_server).create_zip() def load_learning_package(path: str, key: str | None = None, user: UserType | None = None) -> dict: 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 d3e39bda..b1fb52b4 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 @@ -32,11 +32,18 @@ def add_arguments(self, parser): help='The username of the user performing the backup operation.', default=None ) + parser.add_argument( + '--origin_server', + type=str, + help='The origin server for the backup operation.', + default=None + ) def handle(self, *args, **options): lp_key = options['lp_key'] file_name = options['file_name'] username = options['username'] + origin_server = options['origin_server'] if not file_name.lower().endswith(".zip"): raise CommandError("Output file name must end with .zip") try: @@ -45,7 +52,7 @@ def handle(self, *args, **options): if username: user = User.objects.get(username=username) start_time = time.time() - create_zip_file(lp_key, file_name, user=user) + create_zip_file(lp_key, file_name, user=user, origin_server=origin_server) elapsed = time.time() - start_time message = f'{lp_key} written to {file_name} (create_zip_file: {elapsed:.2f} seconds)' self.stdout.write(self.style.SUCCESS(message)) diff --git a/openedx_learning/apps/authoring/backup_restore/zipper.py b/openedx_learning/apps/authoring/backup_restore/zipper.py index a5bc18fd..d50db38e 100644 --- a/openedx_learning/apps/authoring/backup_restore/zipper.py +++ b/openedx_learning/apps/authoring/backup_restore/zipper.py @@ -88,9 +88,25 @@ class LearningPackageZipper: A class to handle the zipping of learning content for backup and restore. """ - def __init__(self, learning_package: LearningPackage, user: UserType | None = None): + def __init__( + self, + learning_package: LearningPackage, + path: str, + user: UserType | None = None, + origin_server: str | None = None): + """ + Initialize the LearningPackageZipper. + + Args: + learning_package (LearningPackage): The learning package to zip. + path (str): The path where the zip file will be created. + user (UserType | None): The user initiating the backup. + origin_server (str | None): The origin server for the backup. + """ self.learning_package = learning_package + self.path = path self.user = user + self.origin_server = origin_server self.folders_already_created: set[Path] = set() self.entities_filenames_already_created: set[str] = set() self.utc_now = datetime.now(tz=timezone.utc) @@ -258,18 +274,18 @@ def get_latest_modified(self, versions_to_check: List[PublishableEntityVersion]) latest = version.created return latest - def create_zip(self, path: str) -> None: + def create_zip(self) -> None: """ Creates a zip file containing the learning package data. - Args: - path (str): The path where the zip file will be created. Raises: Exception: If the learning package cannot be found or if the zip creation fails. """ - with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(self.path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: # Add the package.toml file - package_toml_content: str = toml_learning_package(self.learning_package, self.utc_now, user=self.user) + package_toml_content: str = toml_learning_package( + self.learning_package, self.utc_now, user=self.user, origin_server=self.origin_server + ) self.add_file_to_zip(zipf, Path(TOML_PACKAGE_NAME), package_toml_content, self.learning_package.updated) # Add the entities directory diff --git a/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py b/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py index 968494fb..312f289f 100644 --- a/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py +++ b/tests/openedx_learning/apps/authoring/backup_restore/test_backup.py @@ -9,7 +9,6 @@ from django.contrib.auth import get_user_model from django.core.management import CommandError, call_command from django.db.models import QuerySet -from django.test import override_settings from openedx_learning.api import authoring as api from openedx_learning.api.authoring_models import Collection, Component, Content, LearningPackage, PublishableEntity @@ -215,15 +214,18 @@ def check_zip_file_structure(self, zip_path: Path): for expected_path in expected_paths: self.assertIn(expected_path, zip_name_list) - @override_settings(CMS_BASE="http://cms.test", LMS_BASE="http://lms.test") def test_lp_dump_command(self): lp_key = self.learning_package.key file_name = f"{lp_key}.zip" try: out = StringIO() + origin_server = "http://origin.server" + # Call the management command to dump the learning package - call_command("lp_dump", lp_key, file_name, username=self.user.username, stdout=out) + call_command( + "lp_dump", lp_key, file_name, username=self.user.username, origin_server=origin_server, stdout=out + ) # Check that the zip file was created self.assertTrue(Path(file_name).exists()) @@ -244,6 +246,7 @@ def test_lp_dump_command(self): f'description = "{self.learning_package.description}"', '[meta]', 'format_version = 1', + 'origin_server = "http://origin.server"', 'created_at =', 'created_by = "user"', 'created_by_email = "user@example.com"', @@ -301,7 +304,8 @@ def test_queries_n_plus_problem(self): 1 query for all draft contents 1 query for all published contents """ - zipper = LearningPackageZipper(self.learning_package) + dummy_path = "dummy/path" + zipper = LearningPackageZipper(self.learning_package, dummy_path) entities = zipper.get_publishable_entities() with self.assertNumQueries(3): list(entities) # force evaluation From 9f3f6a1ee6f9ebd992fa3cca4ba15a131b46cd62 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Thu, 13 Nov 2025 12:56:30 -0600 Subject: [PATCH 2/2] fixup! feat: include origin server metadata in backup archive library --- openedx_learning/apps/authoring/backup_restore/api.py | 2 +- openedx_learning/apps/authoring/backup_restore/zipper.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openedx_learning/apps/authoring/backup_restore/api.py b/openedx_learning/apps/authoring/backup_restore/api.py index 0a60fcc8..802bf6ff 100644 --- a/openedx_learning/apps/authoring/backup_restore/api.py +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -17,7 +17,7 @@ def create_zip_file(lp_key: str, path: str, user: UserType | None = None, origin Can throw a NotFoundError at get_learning_package_by_key """ learning_package = get_learning_package_by_key(lp_key) - LearningPackageZipper(learning_package, path, user, origin_server).create_zip() + LearningPackageZipper(learning_package, user, origin_server).create_zip(path) def load_learning_package(path: str, key: str | None = None, user: UserType | None = None) -> dict: diff --git a/openedx_learning/apps/authoring/backup_restore/zipper.py b/openedx_learning/apps/authoring/backup_restore/zipper.py index d50db38e..a2194284 100644 --- a/openedx_learning/apps/authoring/backup_restore/zipper.py +++ b/openedx_learning/apps/authoring/backup_restore/zipper.py @@ -91,7 +91,6 @@ class LearningPackageZipper: def __init__( self, learning_package: LearningPackage, - path: str, user: UserType | None = None, origin_server: str | None = None): """ @@ -99,12 +98,10 @@ def __init__( Args: learning_package (LearningPackage): The learning package to zip. - path (str): The path where the zip file will be created. user (UserType | None): The user initiating the backup. origin_server (str | None): The origin server for the backup. """ self.learning_package = learning_package - self.path = path self.user = user self.origin_server = origin_server self.folders_already_created: set[Path] = set() @@ -274,14 +271,16 @@ def get_latest_modified(self, versions_to_check: List[PublishableEntityVersion]) latest = version.created return latest - def create_zip(self) -> None: + def create_zip(self, path: str) -> None: """ Creates a zip file containing the learning package data. + Args: + path (str): The path where the zip file will be created. Raises: Exception: If the learning package cannot be found or if the zip creation fails. """ - with zipfile.ZipFile(self.path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: # Add the package.toml file package_toml_content: str = toml_learning_package( self.learning_package, self.utc_now, user=self.user, origin_server=self.origin_server