2525 PublishableEntityVersion ,
2626)
2727from openedx_learning .apps .authoring .backup_restore .serializers import (
28+ CollectionSerializer ,
2829 ComponentSerializer ,
2930 ComponentVersionSerializer ,
3031 ContainerSerializer ,
3132 ContainerVersionSerializer ,
3233)
3334from openedx_learning .apps .authoring .backup_restore .toml import (
35+ parse_collection_toml ,
3436 parse_learning_package_toml ,
3537 parse_publishable_entity_toml ,
3638 toml_collection ,
@@ -413,6 +415,7 @@ def __init__(self) -> None:
413415 self .units_map_by_key : dict [str , Any ] = {}
414416 self .subsections_map_by_key : dict [str , Any ] = {}
415417 self .sections_map_by_key : dict [str , Any ] = {}
418+ self .all_publishable_entities_keys : set [str ] = set ()
416419
417420 # --------------------------
418421 # Public API
@@ -434,9 +437,13 @@ def load(self, zipf: zipfile.ZipFile) -> dict[str, Any]:
434437 zipf , organized_files ["containers" ], ContainerSerializer , ContainerVersionSerializer
435438 )
436439
440+ collections_validated = self ._extract_collections (
441+ zipf , organized_files ["collections" ]
442+ )
443+
437444 self ._write_errors ()
438445 if not self .errors :
439- self ._save (learning_package , components_validated , containers_validated )
446+ self ._save (learning_package , components_validated , containers_validated , collections_validated )
440447
441448 return {
442449 "learning_package" : learning_package .key ,
@@ -474,6 +481,7 @@ def _extract_entities(
474481 continue
475482
476483 entity_data = serializer .validated_data
484+ self .all_publishable_entities_keys .add (entity_data ["key" ])
477485 entity_type = entity_data .pop ("container_type" , "components" )
478486 results [entity_type ].append (entity_data )
479487
@@ -491,6 +499,36 @@ def _extract_entities(
491499
492500 return results
493501
502+ def _extract_collections (
503+ self ,
504+ zipf : zipfile .ZipFile ,
505+ collection_files : list [str ],
506+ ) -> dict [str , Any ]:
507+ """Extraction + validation pipeline for collections."""
508+ results : dict [str , list [Any ]] = defaultdict (list )
509+
510+ for file in collection_files :
511+ if not file .endswith (".toml" ):
512+ # Skip non-TOML files
513+ continue
514+ toml_content = self ._read_file_from_zip (zipf , file )
515+ collection_data = parse_collection_toml (toml_content )
516+ serializer = CollectionSerializer (data = {"created_by" : None , ** collection_data })
517+ if not serializer .is_valid ():
518+ self .errors .append ({"file" : file , "errors" : serializer .errors })
519+ continue
520+ collection_validated = serializer .validated_data
521+ entities_list = collection_validated ["entities" ]
522+ for entity_key in entities_list :
523+ if entity_key not in self .all_publishable_entities_keys :
524+ self .errors .append ({
525+ "file" : file ,
526+ "errors" : f"Entity key { entity_key } not found for collection { collection_validated .get ('key' )} "
527+ })
528+ results ["collections" ].append (collection_validated )
529+
530+ return results
531+
494532 # --------------------------
495533 # Save Logic
496534 # --------------------------
@@ -499,7 +537,8 @@ def _save(
499537 self ,
500538 learning_package : LearningPackage ,
501539 components : dict [str , Any ],
502- containers : dict [str , Any ]
540+ containers : dict [str , Any ],
541+ collections : dict [str , Any ]
503542 ) -> None :
504543 """Persist all validated entities in two phases: published then drafts."""
505544
@@ -508,11 +547,23 @@ def _save(
508547 self ._save_units (learning_package , containers )
509548 self ._save_subsections (learning_package , containers )
510549 self ._save_sections (learning_package , containers )
550+ self ._save_collections (learning_package , collections )
511551 publishing_api .publish_all_drafts (learning_package .id )
512552
513553 with publishing_api .bulk_draft_changes_for (learning_package .id ):
514554 self ._save_draft_versions (components , containers )
515555
556+ def _save_collections (self , learning_package , collections ):
557+ """Save collections and their entities."""
558+ for valid_collection in collections .get ("collections" , []):
559+ entities = valid_collection .pop ("entities" , [])
560+ collection = collections_api .create_collection (learning_package .id , ** valid_collection )
561+ collection = collections_api .add_to_collection (
562+ learning_package_id = learning_package .id ,
563+ key = collection .key ,
564+ entities_qset = publishing_api .get_publishable_entities (learning_package .id ).filter (key__in = entities )
565+ )
566+
516567 def _save_components (self , learning_package , components ):
517568 """Save components and published component versions."""
518569 for valid_component in components .get ("components" , []):
0 commit comments