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
96 changes: 88 additions & 8 deletions openedx_learning/apps/authoring/backup_restore/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,32 @@
from openedx_learning.apps.authoring.components import api as components_api


class ComponentSerializer(serializers.Serializer): # pylint: disable=abstract-method
class EntitySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for components.
Contains logic to convert entity_key to component_type and local_key.
Serializer for publishable entities.
"""
can_stand_alone = serializers.BooleanField(required=True)
key = serializers.CharField(required=True)
created = serializers.DateTimeField(required=True)
created_by = serializers.CharField(required=True, allow_null=True)


class EntityVersionSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for publishable entity versions.
"""
title = serializers.CharField(required=True)
entity_key = serializers.CharField(required=True)
created = serializers.DateTimeField(required=True)
created_by = serializers.CharField(required=True, allow_null=True)


class ComponentSerializer(EntitySerializer): # pylint: disable=abstract-method
"""
Serializer for components.
Contains logic to convert entity_key to component_type and local_key.
"""

def validate(self, attrs):
"""
Custom validation logic:
Expand All @@ -31,12 +47,76 @@ def validate(self, attrs):
return attrs


class ComponentVersionSerializer(serializers.Serializer): # pylint: disable=abstract-method
class ComponentVersionSerializer(EntityVersionSerializer): # pylint: disable=abstract-method
"""
Serializer for component versions.
"""
title = serializers.CharField(required=True)
entity_key = serializers.CharField(required=True)
created = serializers.DateTimeField(required=True)
created_by = serializers.CharField(required=True, allow_null=True)
content_to_replace = serializers.DictField(child=serializers.CharField(), required=True)


class ContainerSerializer(EntitySerializer): # pylint: disable=abstract-method
"""
Serializer for containers.
"""
container = serializers.DictField(required=True)

def validate_container(self, value):
"""
Custom validation logic for the container field.
Ensures that the container dict has exactly one key which is one of
"section", "subsection", or "unit" values.
"""
errors = []
if not isinstance(value, dict) or len(value) != 1:
errors.append("Container must be a dict with exactly one key.")
if len(value) == 1: # Only check the key if there is exactly one
container_type = list(value.keys())[0]
if container_type not in ("section", "subsection", "unit"):
errors.append(f"Invalid container value: {container_type}")
if errors:
raise serializers.ValidationError(errors)
return value

def validate(self, attrs):
"""
Custom validation logic:
parse the container dict to extract the container type.
"""
container = attrs["container"]
container_type = list(container.keys())[0] # It is safe to do this after validate_container
attrs["container_type"] = container_type
attrs.pop("container") # Remove the container field after processing
return attrs


class ContainerVersionSerializer(EntityVersionSerializer): # pylint: disable=abstract-method
"""
Serializer for container versions.
"""
container = serializers.DictField(required=True)

def validate_container(self, value):
"""
Custom validation logic for the container field.
Ensures that the container dict has exactly one key "children" which is a list of strings.
"""
errors = []
if not isinstance(value, dict) or len(value) != 1:
errors.append("Container must be a dict with exactly one key.")
if "children" not in value:
errors.append("Container must have a 'children' key.")
if "children" in value and not isinstance(value["children"], list):
errors.append("'children' must be a list.")
if errors:
raise serializers.ValidationError(errors)
return value

def validate(self, attrs):
"""
Custom validation logic:
parse the container dict to extract the children list.
"""
children = attrs["container"]["children"] # It is safe to do this after validate_container
attrs["children"] = children
attrs.pop("container") # Remove the container field after processing
return attrs
25 changes: 17 additions & 8 deletions openedx_learning/apps/authoring/backup_restore/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def _get_toml_publishable_entity_table(
[entity.published]
version_num = 1

[entity.container.section]

Note: This function returns a tomlkit.items.Table, which represents
a string-like TOML fragment rather than a complete TOML document.
"""
Expand All @@ -83,6 +85,18 @@ def _get_toml_publishable_entity_table(
else:
published_table.add(tomlkit.comment("unpublished: no published_version_num"))
entity_table.add("published", published_table)

if hasattr(entity, "container"):
container_table = tomlkit.table()
container_types = ["section", "subsection", "unit"]

for container_type in container_types:
if hasattr(entity.container, container_type):
container_table.add(container_type, tomlkit.table())
break # stop after the first match

entity_table.add("container", container_table)

return entity_table


Expand Down Expand Up @@ -118,12 +132,14 @@ def toml_publishable_entity(

[version.container.unit]
"""
# Create the TOML representation for the entity itself
entity_table = _get_toml_publishable_entity_table(entity, draft_version, published_version)
doc = tomlkit.document()
doc.add("entity", entity_table)

# Add versions as an array of tables (AoT)
doc.add(tomlkit.nl())
doc.add(tomlkit.comment("### Versions"))

for entity_version in versions_to_write:
version = tomlkit.aot()
version_table = toml_publishable_entity_version(entity_version)
Expand Down Expand Up @@ -164,9 +180,6 @@ def toml_publishable_entity_version(version: PublishableEntityVersion) -> tomlki
children = publishing_api.get_container_children_entities_keys(version.containerversion)
container_table.add("children", children)

unit_table = tomlkit.table()

container_table.add("unit", unit_table)
version_table.add("container", container_table)
return version_table # For use in AoT

Expand Down Expand Up @@ -231,8 +244,4 @@ def parse_publishable_entity_toml(content: str) -> tuple[Dict[str, Any], list]:
raise ValueError("Invalid publishable entity TOML: missing 'entity' section")
if "version" not in pe_data:
raise ValueError("Invalid publishable entity TOML: missing 'version' section")
if "key" not in pe_data["entity"]:
raise ValueError("Invalid publishable entity TOML: missing 'key' field")
if "can_stand_alone" not in pe_data["entity"]:
raise ValueError("Invalid publishable entity TOML: missing 'can_stand_alone' field")
return pe_data["entity"], pe_data.get("version", [])
Loading