Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ layers=
# The public authoring API is at the top–none of the apps should call to it.
openedx_learning.api.authoring

# The "backup_restore" app handle the new export and import mechanism.
openedx_learning.apps.authoring.backup_restore

# The "components" app is responsible for storing versioned Components,
# which is Open edX Studio terminology maps to things like individual
# Problems, Videos, and blocks of HTML text. This is also the type we would
Expand All @@ -50,9 +53,6 @@ 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.
Expand Down
1 change: 1 addition & 0 deletions openedx_learning/api/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""
# These wildcard imports are okay because these api modules declare __all__.
# pylint: disable=wildcard-import
from ..apps.authoring.backup_restore.api import *
from ..apps.authoring.collections.api import *
from ..apps.authoring.components.api import *
from ..apps.authoring.contents.api import *
Expand Down
14 changes: 2 additions & 12 deletions openedx_learning/apps/authoring/backup_restore/api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
"""
Backup Restore API
"""
import zipfile

from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackageZipper
from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key

from .toml import TOMLLearningPackageFile

TOML_PACKAGE_NAME = "package.toml"


def create_zip_file(lp_key: str, path: str) -> None:
"""
Expand All @@ -17,9 +12,4 @@ def create_zip_file(lp_key: str, path: str) -> None:
Can throw a NotFoundError at get_learning_package_by_key
"""
learning_package = get_learning_package_by_key(lp_key)
toml_file = TOMLLearningPackageFile(learning_package)
toml_file.create()
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)
LearningPackageZipper(learning_package).create_zip(path)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS(message))
except LearningPackage.DoesNotExist as exc:
message = f"Learning package with key {lp_key} not found"
logger.exception(message)
raise CommandError(message) from exc
except Exception as e:
message = f"Failed to export learning package '{lp_key}': {e}"
Expand Down
109 changes: 55 additions & 54 deletions openedx_learning/apps/authoring/backup_restore/toml.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,73 @@
"""
Utilities for backup and restore app
TOML serialization for learning packages and publishable entities.
"""

from datetime import datetime
from typing import Any, Dict

from tomlkit import comment, document, dumps, nl, table
from tomlkit.items import Table
import tomlkit

from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
from openedx_learning.apps.authoring.publishing.models.publishable_entity import (
PublishableEntityMixin,
PublishableEntityVersionMixin,
)


class TOMLLearningPackageFile():
"""
Class to create a .toml representation of a LearningPackage instance.
def toml_learning_package(learning_package: LearningPackage) -> str:
"""Create a TOML representation of the learning package."""
doc = tomlkit.document()
doc.add(tomlkit.comment(f"Datetime of the export: {datetime.now()}"))
section = tomlkit.table()
section.add("title", learning_package.title)
section.add("key", learning_package.key)
section.add("description", learning_package.description)
section.add("created", learning_package.created)
section.add("updated", learning_package.updated)
doc.add("learning_package", section)
return tomlkit.dumps(doc)

This class builds a structured TOML document using `tomlkit` with metadata and fields
extracted from a `LearningPackage` object. The output can later be saved to a file or used elsewhere.
"""

def __init__(self, learning_package: LearningPackage):
self.doc = document()
self.learning_package = learning_package
def toml_publishable_entity(entity: PublishableEntityMixin) -> str:
"""Create a TOML representation of a publishable entity."""
doc = tomlkit.document()
entity_table = tomlkit.table()
entity_table.add("uuid", str(entity.uuid))
entity_table.add("can_stand_alone", entity.can_stand_alone)

def _create_header(self) -> None:
"""
Adds a comment with the current datetime to indicate when the export occurred.
This helps with traceability and file versioning.
"""
self.doc.add(comment(f"Datetime of the export: {datetime.now()}"))
self.doc.add(nl())
draft = tomlkit.table()
draft.add("version_num", entity.versioning.draft.version_num)
entity_table.add("draft", draft)

def _create_table(self, params: Dict[str, Any]) -> Table:
"""
Builds a TOML table section from a dictionary of key-value pairs.
published = tomlkit.table()
if entity.versioning.published:
published.add("version_num", entity.versioning.published.version_num)
else:
published.add(tomlkit.comment("unpublished: no published_version_num"))
entity_table.add("published", published)

Args:
params (Dict[str, Any]): A dictionary containing keys and values to include in the TOML table.
doc.add("entity", entity_table)
doc.add(tomlkit.nl())
doc.add(tomlkit.comment("### Versions"))

Returns:
Table: A TOML table populated with the provided keys and values.
"""
section = table()
for key, value in params.items():
section.add(key, value)
return section
for entity_version in entity.versioning.versions.all():
version = tomlkit.aot()
version_table = toml_publishable_entity_version(entity_version)
version.append(version_table)
doc.add("version", version)

def create(self) -> None:
"""
Populates the TOML document with a header and a table containing
metadata from the LearningPackage instance.
return tomlkit.dumps(doc)

This method must be called before calling `get()`, otherwise the document will be empty.
"""
self._create_header()
section = self._create_table({
"title": self.learning_package.title,
"key": self.learning_package.key,
"description": self.learning_package.description,
"created": self.learning_package.created,
"updated": self.learning_package.updated
})
self.doc.add("learning_package", section)

def get(self) -> str:
"""
Returns:
str: The string representation of the generated TOML document.
Ensure `create()` has been called beforehand to get meaningful output.
"""
return dumps(self.doc)
def toml_publishable_entity_version(version: PublishableEntityVersionMixin) -> tomlkit.items.Table:
"""Create a TOML representation of a publishable entity version."""
version_table = tomlkit.table()
version_table.add("title", version.title)
version_table.add("uuid", str(version.uuid))
version_table.add("version_num", version.version_num)
container_table = tomlkit.table()
container_table.add("children", [])
unit_table = tomlkit.table()
unit_table.add("graded", True)
container_table.add("unit", unit_table)
version_table.add("container", container_table)
return version_table # For use in AoT
53 changes: 53 additions & 0 deletions openedx_learning/apps/authoring/backup_restore/zipper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
This module provides functionality to create a zip file containing the learning package data,
including a TOML representation of the learning package and its entities.
"""
import zipfile
from pathlib import Path

from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
from openedx_learning.apps.authoring.components import api as components_api
from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage

TOML_PACKAGE_NAME = "package.toml"


class LearningPackageZipper:
"""
A class to handle the zipping of learning content for backup and restore.
"""

def __init__(self, learning_package: LearningPackage):
self.learning_package = learning_package

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.
"""
package_toml_content: str = toml_learning_package(self.learning_package)

with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
# Add the package.toml string
zipf.writestr(TOML_PACKAGE_NAME, package_toml_content)

# Add the entities directory
entities_folder = Path("entities")
zip_info = zipfile.ZipInfo(str(entities_folder) + "/") # Ensure trailing slash
zipf.writestr(zip_info, "") # Add explicit empty directory entry

# Add the collections directory
collections_folder = Path("collections")
collections_info = zipfile.ZipInfo(str(collections_folder) + "/") # Ensure trailing slash
zipf.writestr(collections_info, "") # Add explicit empty directory

# Add each entity's TOML file
for entity in components_api.get_components(self.learning_package.pk):
# Create a TOML representation of the entity
entity_toml_content: str = toml_publishable_entity(entity)
entity_toml_filename = f"{entity.key}.toml"
entity_toml_path = entities_folder / entity_toml_filename
zipf.writestr(str(entity_toml_path), entity_toml_content)
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ def versioning(self):
def uuid(self) -> str:
return self.publishable_entity.uuid

@property
def can_stand_alone(self) -> bool:
return self.publishable_entity.can_stand_alone

@property
def key(self) -> str:
return self.publishable_entity.key
Expand Down
1 change: 1 addition & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def root(*args):
"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",
]

AUTHENTICATION_BACKENDS = [
Expand Down
Empty file.
Loading