Skip to content

Commit 1a7f9b1

Browse files
authored
feat: add original_server, created_by_email to backup archive (#416)
Also bumps version to 0.29.0
1 parent 16f9be8 commit 1a7f9b1

File tree

10 files changed

+71
-23
lines changed

10 files changed

+71
-23
lines changed

openedx_learning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Open edX Learning ("Learning Core").
33
"""
44

5-
__version__ = "0.28.0"
5+
__version__ = "0.29.0"

openedx_learning/apps/authoring/backup_restore/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key
1010

1111

12-
def create_zip_file(lp_key: str, path: str) -> None:
12+
def create_zip_file(lp_key: str, path: str, user: UserType | None = None) -> None:
1313
"""
1414
Creates a dump zip file for the given learning package key at the given path.
1515
The zip file contains a TOML representation of the learning package and its contents.
1616
1717
Can throw a NotFoundError at get_learning_package_by_key
1818
"""
1919
learning_package = get_learning_package_by_key(lp_key)
20-
LearningPackageZipper(learning_package).create_zip(path)
20+
LearningPackageZipper(learning_package, user).create_zip(path)
2121

2222

2323
def load_learning_package(path: str, key: str | None = None, user: UserType | None = None) -> dict:

openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import time
66

7+
from django.contrib.auth import get_user_model
78
from django.core.management import CommandError
89
from django.core.management.base import BaseCommand
910

@@ -13,6 +14,9 @@
1314
logger = logging.getLogger(__name__)
1415

1516

17+
User = get_user_model()
18+
19+
1620
class Command(BaseCommand):
1721
"""
1822
Django management command to export a learning package to a zip file.
@@ -22,15 +26,26 @@ class Command(BaseCommand):
2226
def add_arguments(self, parser):
2327
parser.add_argument('lp_key', type=str, help='The key of the LearningPackage to dump')
2428
parser.add_argument('file_name', type=str, help='The name of the output zip file')
29+
parser.add_argument(
30+
'--username',
31+
type=str,
32+
help='The username of the user performing the backup operation.',
33+
default=None
34+
)
2535

2636
def handle(self, *args, **options):
2737
lp_key = options['lp_key']
2838
file_name = options['file_name']
39+
username = options['username']
2940
if not file_name.lower().endswith(".zip"):
3041
raise CommandError("Output file name must end with .zip")
3142
try:
43+
# Get the user performing the operation
44+
user = None
45+
if username:
46+
user = User.objects.get(username=username)
3247
start_time = time.time()
33-
create_zip_file(lp_key, file_name)
48+
create_zip_file(lp_key, file_name, user=user)
3449
elapsed = time.time() - start_time
3550
message = f'{lp_key} written to {file_name} (create_zip_file: {elapsed:.2f} seconds)'
3651
self.stdout.write(self.style.SUCCESS(message))

openedx_learning/apps/authoring/backup_restore/management/commands/lp_load.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import logging
55
import time
66

7-
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
7+
from django.contrib.auth import get_user_model
88
from django.core.management import CommandError
99
from django.core.management.base import BaseCommand
1010

1111
from openedx_learning.apps.authoring.backup_restore.api import load_learning_package
1212

1313
logger = logging.getLogger(__name__)
1414

15+
User = get_user_model()
16+
1517

1618
class Command(BaseCommand):
1719
"""
@@ -30,8 +32,8 @@ def handle(self, *args, **options):
3032
raise CommandError("Input file name must end with .zip")
3133
try:
3234
start_time = time.time()
33-
# Create a tmp user to pass to the load function
34-
user = UserType.objects.get(username=username)
35+
# Get the user performing the operation
36+
user = User.objects.get(username=username)
3537

3638
result = load_learning_package(file_name, user=user)
3739
duration = time.time() - start_time

openedx_learning/apps/authoring/backup_restore/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class LearningPackageMetadataSerializer(serializers.Serializer): # pylint: disa
3232
"""
3333
format_version = serializers.IntegerField(required=True)
3434
created_by = serializers.CharField(required=False, allow_null=True)
35+
created_by_email = serializers.EmailField(required=False, allow_null=True)
3536
created_at = serializers.DateTimeField(required=True, default_timezone=timezone.utc)
3637
origin_server = serializers.CharField(required=False, allow_null=True)
3738

openedx_learning/apps/authoring/backup_restore/toml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def toml_learning_package(
5353
metadata.add("format_version", format_version)
5454
if user:
5555
metadata.add("created_by", user.username)
56+
metadata.add("created_by_email", user.email)
5657
metadata.add("created_at", timestamp)
5758
if origin_server:
5859
metadata.add("origin_server", origin_server)

openedx_learning/apps/authoring/backup_restore/zipper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ class LearningPackageZipper:
8787
A class to handle the zipping of learning content for backup and restore.
8888
"""
8989

90-
def __init__(self, learning_package: LearningPackage):
90+
def __init__(self, learning_package: LearningPackage, user: UserType | None = None):
9191
self.learning_package = learning_package
92+
self.user = user
9293
self.folders_already_created: set[Path] = set()
9394
self.entities_filenames_already_created: set[str] = set()
9495
self.utc_now = datetime.now(tz=timezone.utc)
@@ -267,7 +268,7 @@ def create_zip(self, path: str) -> None:
267268

268269
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
269270
# Add the package.toml file
270-
package_toml_content: str = toml_learning_package(self.learning_package, self.utc_now)
271+
package_toml_content: str = toml_learning_package(self.learning_package, self.utc_now, user=self.user)
271272
self.add_file_to_zip(zipf, Path(TOML_PACKAGE_NAME), package_toml_content, self.learning_package.updated)
272273

273274
# Add the entities directory
@@ -420,6 +421,7 @@ class BackupMetadata:
420421
format_version: int
421422
created_at: str
422423
created_by: str | None = None
424+
created_by_email: str | None = None
423425
original_server: str | None = None
424426

425427

@@ -587,7 +589,9 @@ def load(self) -> dict[str, Any]:
587589
backup_metadata=BackupMetadata(
588590
format_version=lp_metadata.get("format_version", 1),
589591
created_by=lp_metadata.get("created_by"),
592+
created_by_email=lp_metadata.get("created_by_email"),
590593
created_at=lp_metadata.get("created_at"),
594+
original_server=lp_metadata.get("origin_server"),
591595
) if lp_metadata else None,
592596
)
593597
return asdict(result)

tests/openedx_learning/apps/authoring/backup_restore/fixtures/library_backup/package.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[meta]
22
format_version = 1
3-
created_by = "dormsbee"
3+
created_by = "lp_user"
4+
created_by_email = "[email protected]"
45
created_at = 2025-10-05T18:23:45.180535Z
56
origin_server = "cms.test"
67

tests/openedx_learning/apps/authoring/backup_restore/test_backup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def setUpTestData(cls):
4545
# Create a user for the test
4646
cls.user = User.objects.create(
4747
username="user",
48+
first_name="Learning",
49+
last_name="Package User",
4850
4951
)
5052

@@ -221,7 +223,7 @@ def test_lp_dump_command(self):
221223
out = StringIO()
222224

223225
# Call the management command to dump the learning package
224-
call_command("lp_dump", lp_key, file_name, stdout=out)
226+
call_command("lp_dump", lp_key, file_name, username=self.user.username, stdout=out)
225227

226228
# Check that the zip file was created
227229
self.assertTrue(Path(file_name).exists())
@@ -243,6 +245,8 @@ def test_lp_dump_command(self):
243245
'[meta]',
244246
'format_version = 1',
245247
'created_at =',
248+
'created_by = "user"',
249+
'created_by_email = "[email protected]"',
246250
]
247251
)
248252

tests/openedx_learning/apps/authoring/backup_restore/test_restore.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from io import StringIO
55
from unittest.mock import patch
66

7-
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
7+
from django.contrib.auth import get_user_model
88
from django.core.management import call_command
99

1010
from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackageUnzipper, generate_staged_lp_key
@@ -14,16 +14,22 @@
1414
from openedx_learning.lib.test_utils import TestCase
1515
from test_utils.zip_file_utils import folder_to_inmemory_zip
1616

17+
User = get_user_model()
1718

18-
class RestoreLearningPackageCommandTest(TestCase):
19-
"""Tests for the lp_load management command."""
19+
20+
class RestoreTestCase(TestCase):
21+
"""Base test case for restore tests."""
2022

2123
def setUp(self):
2224
super().setUp()
2325
self.fixtures_folder = os.path.join(os.path.dirname(__file__), "fixtures/library_backup")
2426
self.zip_file = folder_to_inmemory_zip(self.fixtures_folder)
2527
self.lp_key = "lib:WGU:LIB_C001"
26-
self.user = UserType.objects.create_user(username='lp_user', password='12345')
28+
self.user = User.objects.create_user(username='lp_user', password='12345')
29+
30+
31+
class RestoreLearningPackageCommandTest(RestoreTestCase):
32+
"""Tests for the lp_load management command."""
2733

2834
@patch("openedx_learning.apps.authoring.backup_restore.api.load_learning_package")
2935
def test_restore_command(self, mock_load_learning_package):
@@ -152,13 +158,12 @@ def verify_collections(self, lp):
152158
assert set(entity_keys) == set(expected_entity_keys)
153159

154160

155-
class RestoreLearningPackageTest(TestCase):
161+
class RestoreLearningPackageTest(RestoreTestCase):
156162
"""Tests for restoring learning packages without using the management command."""
157163

158164
def test_successful_restore_with_no_command_line(self):
159165
"""Test restoring a learning package without using the management command."""
160-
zip_file = folder_to_inmemory_zip(os.path.join(os.path.dirname(__file__), "fixtures/library_backup"))
161-
result = LearningPackageUnzipper(zip_file, key="lib-xx:WGU:LIB_C001").load()
166+
result = LearningPackageUnzipper(self.zip_file, key="lib-xx:WGU:LIB_C001").load()
162167

163168
expected = {
164169
"status": "success",
@@ -179,7 +184,7 @@ def test_successful_restore_with_no_command_line(self):
179184
},
180185
"backup_metadata": {
181186
"format_version": 1,
182-
"created_by": "dormsbee",
187+
"created_by": "lp_user",
183188
"created_at": datetime(2025, 10, 5, 18, 23, 45, 180535, tzinfo=timezone.utc),
184189
"origin_server": "cms.test",
185190
},
@@ -202,9 +207,7 @@ def test_successful_restore_with_no_command_line(self):
202207

203208
def test_successful_restore_with_staged_key(self):
204209
"""Test restoring a learning package with a staged key."""
205-
user = UserType.objects.create_user(username='lp_user', password='12345')
206-
zip_file = folder_to_inmemory_zip(os.path.join(os.path.dirname(__file__), "fixtures/library_backup"))
207-
result = LearningPackageUnzipper(zip_file, user=user).load()
210+
result = LearningPackageUnzipper(self.zip_file, user=self.user).load()
208211

209212
assert result["status"] == "success"
210213
assert result["lp_restored_data"] is not None
@@ -254,7 +257,7 @@ def test_error_learning_package_missing_key(self):
254257
},
255258
"meta": {
256259
"format_version": 1,
257-
"created_by": "dormsbee",
260+
"created_by": "lp_user",
258261
"created_at": "2025-09-03T17:50:59.536190Z",
259262
"origin_server": "cms.test",
260263
},
@@ -295,6 +298,23 @@ def test_error_no_metadata_section(self):
295298
expected_error = "Errors encountered during restore:\npackage.toml meta section: {'non_field_errors': [Er"
296299
assert expected_error in log_content
297300

301+
def test_success_metadata_using_user_context(self):
302+
"""Test that metadata is correctly extracted from learning_package.toml."""
303+
restore_result = LearningPackageUnzipper(self.zip_file, user=self.user).load()
304+
metadata = restore_result.get("backup_metadata", {})
305+
306+
assert restore_result["status"] == "success"
307+
308+
expected_metadata = {
309+
"format_version": 1,
310+
"created_by": "lp_user",
311+
"created_by_email": "[email protected]",
312+
"created_at": datetime(2025, 10, 5, 18, 23, 45, 180535, tzinfo=timezone.utc),
313+
"original_server": "cms.test",
314+
}
315+
316+
assert metadata == expected_metadata
317+
298318

299319
class RestoreUtilitiesTest(TestCase):
300320
"""Tests for utility functions used in the restore process."""

0 commit comments

Comments
 (0)