@@ -406,8 +406,9 @@ class LearningPackageUnzipper:
406406 summary = unzipper.load("/path/to/backup.zip")
407407 """
408408
409- def __init__ (self ) -> None :
410- self .utc_now : datetime = datetime .now (tz = timezone .utc )
409+ def __init__ (self , zipf : zipfile .ZipFile ) -> None :
410+ self .zipf = zipf
411+ self .utc_now : datetime = datetime .now (timezone .utc )
411412 self .component_types_cache : dict [tuple [str , str ], ComponentType ] = {}
412413 self .errors : list [dict [str , Any ]] = []
413414 # Maps for resolving relationships
@@ -422,28 +423,34 @@ def __init__(self) -> None:
422423 # --------------------------
423424
424425 @transaction .atomic
425- def load (self , zipf : zipfile . ZipFile ) -> dict [str , Any ]:
426+ def load (self ) -> dict [str , Any ]:
426427 """Extracts and restores all objects from the ZIP archive in an atomic transaction."""
427- organized_files = self ._get_organized_file_list (zipf .namelist ())
428+ organized_files = self ._get_organized_file_list (self . zipf .namelist ())
428429
429430 if not organized_files ["learning_package" ]:
430431 raise FileNotFoundError (f"Missing required { TOML_PACKAGE_NAME } in archive." )
431432
432- learning_package = self ._load_learning_package (zipf , organized_files ["learning_package" ])
433+ learning_package = self ._load_learning_package (organized_files ["learning_package" ])
433434 components_validated = self ._extract_entities (
434- zipf , organized_files ["components" ], ComponentSerializer , ComponentVersionSerializer
435+ organized_files ["components" ], ComponentSerializer , ComponentVersionSerializer
435436 )
436437 containers_validated = self ._extract_entities (
437- zipf , organized_files ["containers" ], ContainerSerializer , ContainerVersionSerializer
438+ organized_files ["containers" ], ContainerSerializer , ContainerVersionSerializer
438439 )
439440
440441 collections_validated = self ._extract_collections (
441- zipf , organized_files ["collections" ]
442+ organized_files ["collections" ]
442443 )
443444
444445 self ._write_errors ()
445446 if not self .errors :
446- self ._save (learning_package , components_validated , containers_validated , collections_validated )
447+ self ._save (
448+ learning_package ,
449+ components_validated ,
450+ containers_validated ,
451+ collections_validated ,
452+ component_static_files = organized_files ["component_static_files" ]
453+ )
447454
448455 return {
449456 "learning_package" : learning_package .key ,
@@ -458,7 +465,6 @@ def load(self, zipf: zipfile.ZipFile) -> dict[str, Any]:
458465
459466 def _extract_entities (
460467 self ,
461- zipf : zipfile .ZipFile ,
462468 entity_files : list [str ],
463469 entity_serializer : type [serializers .Serializer ],
464470 version_serializer : type [serializers .Serializer ],
@@ -471,7 +477,7 @@ def _extract_entities(
471477 # Skip non-TOML files
472478 continue
473479
474- entity_data , draft_version , published_version = self ._load_entity_data (zipf , file )
480+ entity_data , draft_version , published_version = self ._load_entity_data (file )
475481 serializer = entity_serializer (
476482 data = {"created" : self .utc_now , "created_by" : None , ** entity_data }
477483 )
@@ -501,7 +507,6 @@ def _extract_entities(
501507
502508 def _extract_collections (
503509 self ,
504- zipf : zipfile .ZipFile ,
505510 collection_files : list [str ],
506511 ) -> dict [str , Any ]:
507512 """Extraction + validation pipeline for collections."""
@@ -511,7 +516,7 @@ def _extract_collections(
511516 if not file .endswith (".toml" ):
512517 # Skip non-TOML files
513518 continue
514- toml_content = self ._read_file_from_zip (zipf , file )
519+ toml_content = self ._read_file_from_zip (file )
515520 collection_data = parse_collection_toml (toml_content )
516521 serializer = CollectionSerializer (data = {"created_by" : None , ** collection_data })
517522 if not serializer .is_valid ():
@@ -538,20 +543,22 @@ def _save(
538543 learning_package : LearningPackage ,
539544 components : dict [str , Any ],
540545 containers : dict [str , Any ],
541- collections : dict [str , Any ]
546+ collections : dict [str , Any ],
547+ * ,
548+ component_static_files : dict [str , List [str ]]
542549 ) -> None :
543550 """Persist all validated entities in two phases: published then drafts."""
544551
545552 with publishing_api .bulk_draft_changes_for (learning_package .id ):
546- self ._save_components (learning_package , components )
553+ self ._save_components (learning_package , components , component_static_files )
547554 self ._save_units (learning_package , containers )
548555 self ._save_subsections (learning_package , containers )
549556 self ._save_sections (learning_package , containers )
550557 self ._save_collections (learning_package , collections )
551558 publishing_api .publish_all_drafts (learning_package .id )
552559
553560 with publishing_api .bulk_draft_changes_for (learning_package .id ):
554- self ._save_draft_versions (components , containers )
561+ self ._save_draft_versions (components , containers , component_static_files )
555562
556563 def _save_collections (self , learning_package , collections ):
557564 """Save collections and their entities."""
@@ -564,7 +571,7 @@ def _save_collections(self, learning_package, collections):
564571 entities_qset = publishing_api .get_publishable_entities (learning_package .id ).filter (key__in = entities )
565572 )
566573
567- def _save_components (self , learning_package , components ):
574+ def _save_components (self , learning_package , components , component_static_files ):
568575 """Save components and published component versions."""
569576 for valid_component in components .get ("components" , []):
570577 entity_key = valid_component .pop ("key" )
@@ -573,8 +580,11 @@ def _save_components(self, learning_package, components):
573580
574581 for valid_published in components .get ("components_published" , []):
575582 entity_key = valid_published .pop ("entity_key" )
583+ content_to_replace = self ._resolve_static_files (valid_published , entity_key , component_static_files )
576584 components_api .create_next_component_version (
577585 self .components_map_by_key [entity_key ].publishable_entity .id ,
586+ content_to_replace = content_to_replace ,
587+ force_version_num = valid_published .pop ("version_num" , None ),
578588 ** valid_published
579589 )
580590
@@ -620,34 +630,47 @@ def _save_sections(self, learning_package, containers):
620630 self .sections_map_by_key [entity_key ], subsections = children , ** valid_published
621631 )
622632
623- def _save_draft_versions (self , components , containers ):
633+ def _save_draft_versions (self , components , containers , component_static_files ):
624634 """Save draft versions for all entity types."""
625635 for valid_draft in components .get ("components_drafts" , []):
626636 entity_key = valid_draft .pop ("entity_key" )
637+ content_to_replace = self ._resolve_static_files (valid_draft , entity_key , component_static_files )
638+ print ("Entity key" , valid_draft .get ("version_num" , "-- NO TIENE" ), valid_draft )
627639 components_api .create_next_component_version (
628640 self .components_map_by_key [entity_key ].publishable_entity .id ,
641+ content_to_replace = content_to_replace ,
642+ force_version_num = valid_draft .pop ("version_num" , None ),
629643 ** valid_draft
630644 )
631645
632646 for valid_draft in containers .get ("unit_drafts" , []):
633647 entity_key = valid_draft .pop ("entity_key" )
634648 children = self ._resolve_children (valid_draft , self .components_map_by_key )
635649 units_api .create_next_unit_version (
636- self .units_map_by_key [entity_key ], components = children , ** valid_draft
650+ self .units_map_by_key [entity_key ],
651+ components = children ,
652+ force_version_num = valid_draft .pop ("version_num" , None ),
653+ ** valid_draft
637654 )
638655
639656 for valid_draft in containers .get ("subsection_drafts" , []):
640657 entity_key = valid_draft .pop ("entity_key" )
641658 children = self ._resolve_children (valid_draft , self .units_map_by_key )
642659 subsections_api .create_next_subsection_version (
643- self .subsections_map_by_key [entity_key ], units = children , ** valid_draft
660+ self .subsections_map_by_key [entity_key ],
661+ units = children ,
662+ force_version_num = valid_draft .pop ("version_num" , None ),
663+ ** valid_draft
644664 )
645665
646666 for valid_draft in containers .get ("section_drafts" , []):
647667 entity_key = valid_draft .pop ("entity_key" )
648668 children = self ._resolve_children (valid_draft , self .subsections_map_by_key )
649669 sections_api .create_next_section_version (
650- self .sections_map_by_key [entity_key ], subsections = children , ** valid_draft
670+ self .sections_map_by_key [entity_key ],
671+ subsections = children ,
672+ force_version_num = valid_draft .pop ("version_num" , None ),
673+ ** valid_draft
651674 )
652675
653676 # --------------------------
@@ -680,14 +703,35 @@ def _write_errors(self) -> str | None:
680703
681704 return log_filename
682705
706+ def _resolve_static_files (
707+ self ,
708+ version_data : dict [str , Any ],
709+ entity_key : str ,
710+ static_files_map : dict [str , List [str ]]
711+ ) -> dict [str , bytes ]:
712+ """Resolve static file paths into their binary content."""
713+ resolved_files : dict [str , bytes ] = {}
714+ num_version = version_data .get ("version_num" )
715+
716+ if not num_version :
717+ return resolved_files
718+
719+ static_file_key = f"{ entity_key } :v{ num_version } " # e.g., "my_component:123:v1"
720+ static_files = static_files_map .get (static_file_key , [])
721+ for static_file in static_files :
722+ local_key = static_file .split (f"v{ num_version } /" )[- 1 ]
723+ with self .zipf .open (static_file , "r" ) as f :
724+ resolved_files [local_key ] = f .read ()
725+ return resolved_files
726+
683727 def _resolve_children (self , entity_data : dict [str , Any ], lookup_map : dict [str , Any ]) -> list [Any ]:
684728 """Resolve child entity keys into model instances."""
685729 children_keys = entity_data .pop ("children" , [])
686730 return [lookup_map [key ] for key in children_keys if key in lookup_map ]
687731
688- def _load_learning_package (self , zipf : zipfile . ZipFile , package_file : str ) -> LearningPackage :
732+ def _load_learning_package (self , package_file : str ) -> LearningPackage :
689733 """Load and persist the learning package TOML file."""
690- toml_content = self ._read_file_from_zip (zipf , package_file )
734+ toml_content = self ._read_file_from_zip (package_file )
691735 data = parse_learning_package_toml (toml_content )
692736 return publishing_api .create_learning_package (
693737 key = data ["key" ],
@@ -696,10 +740,10 @@ def _load_learning_package(self, zipf: zipfile.ZipFile, package_file: str) -> Le
696740 )
697741
698742 def _load_entity_data (
699- self , zipf : zipfile . ZipFile , entity_file : str
743+ self , entity_file : str
700744 ) -> tuple [dict [str , Any ], dict [str , Any ] | None , dict [str , Any ] | None ]:
701745 """Load entity data and its versions from TOML."""
702- content = self ._read_file_from_zip (zipf , entity_file )
746+ content = self ._read_file_from_zip (entity_file )
703747 entity_data , version_data = parse_publishable_entity_toml (content )
704748 return entity_data , * self ._get_versions_to_write (version_data , entity_data )
705749
@@ -712,7 +756,6 @@ def _validate_versions(self, entity_data, draft, published, serializer_cls, *, f
712756 serializer = serializer_cls (
713757 data = {
714758 "entity_key" : entity_data ["key" ],
715- "content_to_replace" : {},
716759 "created" : self .utc_now ,
717760 "created_by" : None ,
718761 ** version
@@ -724,9 +767,9 @@ def _validate_versions(self, entity_data, draft, published, serializer_cls, *, f
724767 self .errors .append ({"file" : file , "errors" : serializer .errors })
725768 return valid
726769
727- def _read_file_from_zip (self , zipf : zipfile . ZipFile , filename : str ) -> str :
770+ def _read_file_from_zip (self , filename : str ) -> str :
728771 """Read and decode a UTF-8 file from the zip archive."""
729- with zipf .open (filename ) as f :
772+ with self . zipf .open (filename ) as f :
730773 return f .read ().decode ("utf-8" )
731774
732775 def _get_organized_file_list (self , file_paths : list [str ]) -> dict [str , Any ]:
@@ -735,6 +778,7 @@ def _get_organized_file_list(self, file_paths: list[str]) -> dict[str, Any]:
735778 "learning_package" : None ,
736779 "containers" : [],
737780 "components" : [],
781+ "component_static_files" : defaultdict (list ),
738782 "collections" : [],
739783 }
740784
@@ -746,10 +790,19 @@ def _get_organized_file_list(self, file_paths: list[str]) -> dict[str, Any]:
746790 elif path .startswith ("entities/" ) and str (Path (path ).parent ) == "entities" :
747791 organized ["containers" ].append (path )
748792 elif path .startswith ("entities/" ):
749- organized ["components" ].append (path )
793+ if path .endswith (".toml" ):
794+ organized ["components" ].append (path )
795+ else :
796+ component_key = Path (path ).parts [1 :4 ] # e.g., ['xblock.v1', 'html', 'my_component_123456']
797+ num_version = Path (path ).parts [5 ] if len (Path (path ).parts ) > 5 else "v1" # e.g., 'v1'
798+ if len (component_key ) == 3 :
799+ component_identifier = ":" .join (component_key )
800+ component_identifier += f":{ num_version } "
801+ organized ["component_static_files" ][component_identifier ].append (path )
802+ else :
803+ self .errors .append ({"file" : path , "errors" : "Invalid component static file path structure." })
750804 elif path .startswith ("collections/" ):
751805 organized ["collections" ].append (path )
752-
753806 return organized
754807
755808 def _get_versions_to_write (
0 commit comments