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..43527c77b --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/api.py @@ -0,0 +1,20 @@ +""" +Backup Restore API +""" +import zipfile + +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 = TOMLLearningPackageFile() + 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..7aa3f022b --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/apps.py @@ -0,0 +1,12 @@ +""" +Backup/Restore 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/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..fa0de8579 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py @@ -0,0 +1,37 @@ +""" +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 to 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') + 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'{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 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/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/toml.py b/openedx_learning/apps/authoring/backup_restore/toml.py new file mode 100644 index 000000000..01ae71cc5 --- /dev/null +++ b/openedx_learning/apps/authoring/backup_restore/toml.py @@ -0,0 +1,45 @@ +""" +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 TOMLLearningPackageFile(): + """ + 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()) + + 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 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