From 33a914c0b2a230b80e7e91a39ea072cd8916a2eb Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Wed, 3 Dec 2025 13:07:46 -0600 Subject: [PATCH 1/2] fix: add missing author value when restoring entities --- .../authoring/backup_restore/serializers.py | 3 -- .../apps/authoring/backup_restore/zipper.py | 23 +++++++++++--- .../authoring/backup_restore/test_restore.py | 31 ++++++++++++++++++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/openedx_learning/apps/authoring/backup_restore/serializers.py b/openedx_learning/apps/authoring/backup_restore/serializers.py index 86f664b4..c34e8105 100644 --- a/openedx_learning/apps/authoring/backup_restore/serializers.py +++ b/openedx_learning/apps/authoring/backup_restore/serializers.py @@ -44,7 +44,6 @@ class EntitySerializer(serializers.Serializer): # pylint: disable=abstract-meth can_stand_alone = serializers.BooleanField(required=True) key = serializers.CharField(required=True) created = serializers.DateTimeField(required=True, default_timezone=timezone.utc) - created_by = serializers.CharField(required=True, allow_null=True) class EntityVersionSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -54,7 +53,6 @@ class EntityVersionSerializer(serializers.Serializer): # pylint: disable=abstra title = serializers.CharField(required=True) entity_key = serializers.CharField(required=True) created = serializers.DateTimeField(required=True, default_timezone=timezone.utc) - created_by = serializers.CharField(required=True, allow_null=True) version_num = serializers.IntegerField(required=True) @@ -160,7 +158,6 @@ class CollectionSerializer(serializers.Serializer): # pylint: disable=abstract- title = serializers.CharField(required=True) key = serializers.CharField(required=True) description = serializers.CharField(required=True, allow_blank=True) - created_by = serializers.IntegerField(required=True, allow_null=True) entities = serializers.ListField( child=serializers.CharField(), required=True, diff --git a/openedx_learning/apps/authoring/backup_restore/zipper.py b/openedx_learning/apps/authoring/backup_restore/zipper.py index a2194284..27ddcacc 100644 --- a/openedx_learning/apps/authoring/backup_restore/zipper.py +++ b/openedx_learning/apps/authoring/backup_restore/zipper.py @@ -508,6 +508,7 @@ class LearningPackageUnzipper: def __init__(self, zipf: zipfile.ZipFile, key: str | None = None, user: UserType | None = None): self.zipf = zipf self.user = user + self.user_id = getattr(self.user, "id", None) self.lp_key = key # If provided, use this key for the restored learning package self.learning_package_id: int | None = None # Will be set upon restoration self.utc_now: datetime = datetime.now(timezone.utc) @@ -771,7 +772,9 @@ def _save_collections(self, learning_package, collections): """Save collections and their entities.""" for valid_collection in collections.get("collections", []): entities = valid_collection.pop("entities", []) - collection = collections_api.create_collection(learning_package.id, **valid_collection) + collection = collections_api.create_collection( + learning_package.id, created_by=self.user_id, **valid_collection + ) collection = collections_api.add_to_collection( learning_package_id=learning_package.id, key=collection.key, @@ -782,7 +785,7 @@ def _save_components(self, learning_package, components, component_static_files) """Save components and published component versions.""" for valid_component in components.get("components", []): entity_key = valid_component.pop("key") - component = components_api.create_component(learning_package.id, **valid_component) + component = components_api.create_component(learning_package.id, created_by=self.user_id, **valid_component) self.components_map_by_key[entity_key] = component for valid_published in components.get("components_published", []): @@ -796,6 +799,7 @@ def _save_components(self, learning_package, components, component_static_files) self.components_map_by_key[entity_key].publishable_entity.id, content_to_replace=content_to_replace, force_version_num=valid_published.pop("version_num", None), + created_by=self.user_id, **valid_published ) @@ -803,7 +807,7 @@ def _save_units(self, learning_package, containers): """Save units and published unit versions.""" for valid_unit in containers.get("unit", []): entity_key = valid_unit.get("key") - unit = units_api.create_unit(learning_package.id, **valid_unit) + unit = units_api.create_unit(learning_package.id, created_by=self.user_id, **valid_unit) self.units_map_by_key[entity_key] = unit for valid_published in containers.get("unit_published", []): @@ -816,6 +820,7 @@ def _save_units(self, learning_package, containers): self.units_map_by_key[entity_key], force_version_num=valid_published.pop("version_num", None), components=children, + created_by=self.user_id, **valid_published ) @@ -823,7 +828,9 @@ def _save_subsections(self, learning_package, containers): """Save subsections and published subsection versions.""" for valid_subsection in containers.get("subsection", []): entity_key = valid_subsection.get("key") - subsection = subsections_api.create_subsection(learning_package.id, **valid_subsection) + subsection = subsections_api.create_subsection( + learning_package.id, created_by=self.user_id, **valid_subsection + ) self.subsections_map_by_key[entity_key] = subsection for valid_published in containers.get("subsection_published", []): @@ -836,6 +843,7 @@ def _save_subsections(self, learning_package, containers): self.subsections_map_by_key[entity_key], units=children, force_version_num=valid_published.pop("version_num", None), + created_by=self.user_id, **valid_published ) @@ -843,7 +851,7 @@ def _save_sections(self, learning_package, containers): """Save sections and published section versions.""" for valid_section in containers.get("section", []): entity_key = valid_section.get("key") - section = sections_api.create_section(learning_package.id, **valid_section) + section = sections_api.create_section(learning_package.id, created_by=self.user_id, **valid_section) self.sections_map_by_key[entity_key] = section for valid_published in containers.get("section_published", []): @@ -856,6 +864,7 @@ def _save_sections(self, learning_package, containers): self.sections_map_by_key[entity_key], subsections=children, force_version_num=valid_published.pop("version_num", None), + created_by=self.user_id, **valid_published ) @@ -874,6 +883,7 @@ def _save_draft_versions(self, components, containers, component_static_files): # Drafts can diverge from published, so we allow ignoring previous content # Use case: published v1 had files A, B; draft v2 only has file A ignore_previous_content=True, + created_by=self.user_id, **valid_draft ) @@ -887,6 +897,7 @@ def _save_draft_versions(self, components, containers, component_static_files): self.units_map_by_key[entity_key], components=children, force_version_num=valid_draft.pop("version_num", None), + created_by=self.user_id, **valid_draft ) @@ -900,6 +911,7 @@ def _save_draft_versions(self, components, containers, component_static_files): self.subsections_map_by_key[entity_key], units=children, force_version_num=valid_draft.pop("version_num", None), + created_by=self.user_id, **valid_draft ) @@ -913,6 +925,7 @@ def _save_draft_versions(self, components, containers, component_static_files): self.sections_map_by_key[entity_key], subsections=children, force_version_num=valid_draft.pop("version_num", None), + created_by=self.user_id, **valid_draft ) diff --git a/tests/openedx_learning/apps/authoring/backup_restore/test_restore.py b/tests/openedx_learning/apps/authoring/backup_restore/test_restore.py index de2d13c1..e34a0b5f 100644 --- a/tests/openedx_learning/apps/authoring/backup_restore/test_restore.py +++ b/tests/openedx_learning/apps/authoring/backup_restore/test_restore.py @@ -34,7 +34,7 @@ class RestoreLearningPackageCommandTest(RestoreTestCase): @patch("openedx_learning.apps.authoring.backup_restore.api.load_learning_package") def test_restore_command(self, mock_load_learning_package): # Mock load_learning_package to return our in-memory zip file - restore_result = LearningPackageUnzipper(self.zip_file, self.user).load() + restore_result = LearningPackageUnzipper(self.zip_file, user=self.user).load() mock_load_learning_package.return_value = restore_result out = StringIO() @@ -63,20 +63,28 @@ def verify_containers(self, lp): assert container.key in expected_container_keys draft_version = publishing_api.get_draft_version(container.publishable_entity.id) published_version = publishing_api.get_published_version(container.publishable_entity.id) + assert container.created_by is not None + assert container.created_by.username == "lp_user" if container.key == "unit1-b7eafb": assert getattr(container, 'unit', None) is not None assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None elif container.key == "subsection1-48afa3": assert getattr(container, 'subsection', None) is not None assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None elif container.key == "section1-8ca126": assert getattr(container, 'section', None) is not None assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None else: assert False, f"Unexpected container key: {container.key}" @@ -98,11 +106,15 @@ def verify_components(self, lp): assert component.key in expected_component_keys draft_version = publishing_api.get_draft_version(component.publishable_entity.id) published_version = publishing_api.get_published_version(component.publishable_entity.id) + assert component.created_by is not None + assert component.created_by.username == "lp_user" if component.key == "xblock.v1:drag-and-drop-v2:4d1b2fac-8b30-42fb-872d-6b10ab580b27": assert component.component_type.name == "drag-and-drop-v2" assert component.component_type.namespace == "xblock.v1" assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None # Get the content associated with this component contents = draft_version.componentversion.contents.all() @@ -116,19 +128,27 @@ def verify_components(self, lp): assert component.component_type.namespace == "xblock.v1" assert draft_version is not None assert draft_version.version_num == 5 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is not None assert published_version.version_num == 4 + assert published_version.created_by is not None + assert published_version.created_by.username == "lp_user" elif component.key == "xblock.v1:openassessment:1ee38208-a585-4455-a27e-4930aa541f53": assert component.component_type.name == "openassessment" assert component.component_type.namespace == "xblock.v1" assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None elif component.key == "xblock.v1:problem:256739e8-c2df-4ced-bd10-8156f6cfa90b": assert component.component_type.name == "problem" assert component.component_type.namespace == "xblock.v1" assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None elif component.key == "xblock.v1:survey:6681da3f-b056-4c6e-a8f9-040967907471": assert component.component_type.name == "survey" @@ -141,12 +161,18 @@ def verify_components(self, lp): assert component.component_type.namespace == "xblock.v1" assert draft_version is not None assert draft_version.version_num == 3 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is None elif component.key == "xblock.v1:html:c22b9f97-f1e9-4e8f-87f0-d5a3c26083e2": assert draft_version is not None assert draft_version.version_num == 2 + assert draft_version.created_by is not None + assert draft_version.created_by.username == "lp_user" assert published_version is not None assert published_version.version_num == 2 + assert published_version.created_by is not None + assert published_version.created_by.username == "lp_user" else: assert False, f"Unexpected component key: {component.key}" @@ -158,6 +184,9 @@ def verify_collections(self, lp): assert collection.title == "Collection test1" assert collection.key == "collection-test" assert collection.description == "" + assert collection.created_by is not None + assert collection.created_by.username == "lp_user" + expected_entity_keys = [ "xblock.v1:html:e32d5479-9492-41f6-9222-550a7346bc37", "xblock.v1:problem:256739e8-c2df-4ced-bd10-8156f6cfa90b", From b9a96ee385ae6b0884f270c2c8d33e39db4c17f6 Mon Sep 17 00:00:00 2001 From: Daniel Wong Date: Wed, 3 Dec 2025 13:19:33 -0600 Subject: [PATCH 2/2] fixup! fix: add missing author value when restoring entities --- openedx_learning/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index 12091801..c7fcd06b 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -2,4 +2,4 @@ Open edX Learning ("Learning Core"). """ -__version__ = "0.30.1" +__version__ = "0.30.2"