2121from ..exceptions import InvalidNodeException
2222from ..utils .utils import is_valid_uuid_string
2323from .licenses import License
24+ from ricecooker .utils .SCORM_metadata import update_node_from_metadata
2425
2526MASTERY_MODELS = [id for id , name in exercises .MASTERY_MODELS ]
2627ROLES = [id for id , name in roles .choices ]
@@ -31,6 +32,7 @@ class Node(object):
3132
3233 license = None
3334 language = None
35+ kind = None
3436
3537 def __init__ (
3638 self ,
@@ -40,7 +42,7 @@ def __init__(
4042 thumbnail = None ,
4143 files = None ,
4244 derive_thumbnail = False ,
43- node_modifications = {} ,
45+ node_modifications = None ,
4446 extra_fields = None ,
4547 ):
4648 self .files = []
@@ -60,7 +62,7 @@ def __init__(
6062
6163 self .set_thumbnail (thumbnail )
6264 # save modifications passed in by csv
63- self .node_modifications = node_modifications
65+ self .node_modifications = node_modifications or {}
6466
6567 def set_language (self , language ):
6668 """Set self.language to internal lang. repr. code from str or Language object."""
@@ -1649,54 +1651,87 @@ def to_dict(self):
16491651RemoteContentNode = StudioContentNode
16501652
16511653
1652- def get_by_path (obj , * args ):
1653- for arg in args :
1654- obj = obj .get (arg , {})
1655- if obj == {}:
1656- return None
1657- return obj
1658-
1659-
1660- QTI_PLACEHOLDER_SOURCE_ID = "A SOURCE ID THAT WILL BE REMOVED BEFORE UPLOAD"
1661-
1654+ class IMSCPNode (TreeNode ):
16621655
1663- class QTINode (ContentNode ):
1664- """
1665- Node representing QTI exercise
1666- """
1656+ kind = content_kinds .HTML5
16671657
1668- kind = content_kinds .EXERCISE
1658+ @classmethod
1659+ def _recurse_and_add_children (cls , parent , item_data , license ):
1660+ source_id = item_data .get ("identifier" , item_data ["title" ])
1661+ if item_data .get ("children" ) is not None :
1662+ node = TopicNode (
1663+ source_id ,
1664+ item_data ["title" ],
1665+ files = parent .files ,
1666+ )
1667+ for child in item_data ["children" ]:
1668+ cls ._recurse_and_add_children (node , child , license )
1669+ else :
1670+ node = ContentNode (
1671+ source_id ,
1672+ item_data ["title" ],
1673+ license ,
1674+ files = parent .files ,
1675+ extra_fields = {
1676+ "options" : {
1677+ "entry" : item_data ["href" ] + item_data .get ("parameters" , "" )
1678+ }
1679+ },
1680+ )
1681+ node .kind = cls .kind
1682+ update_node_from_metadata (node , item_data ["metadata" ])
1683+ parent .add_child (node )
16691684
1670- def __init__ (
1671- self , source_id = QTI_PLACEHOLDER_SOURCE_ID , title = None , license = None , ** kwargs
1672- ):
1673- super (QTINode , self ).__init__ (source_id , title , license , ** kwargs )
1674- from .files import QTIZipFile
1685+ def __new__ (cls , * args , ** kwargs ):
1686+ from .files import IMSCPZipFile
16751687
1676- qti_files = [f for f in self . files if isinstance (f , QTIZipFile )]
1677- qti_file = qti_files [ 0 ]
1678- qti_file . process_file ()
1679- metadata = qti_file . extract_metadata ()
1680- self . source_id = (
1681- metadata . get ( "identifer" )
1682- if self . source_id == QTI_PLACEHOLDER_SOURCE_ID
1683- else self . source_id
1684- )
1685- if self . source_id is None :
1688+ imscp_files = [f for f in kwargs [ " files" ] if isinstance (f , IMSCPZipFile )]
1689+ if not imscp_files or len ( imscp_files ) > 1 :
1690+ raise InvalidNodeException (
1691+ "IMSCPNode must be instantiated with exactly one IMSCPZipFile"
1692+ )
1693+ imscp_file = imscp_files [ 0 ]
1694+ metadata = imscp_file . extract_metadata ()
1695+ if "title" in metadata [ "metadata" ] and kwargs . get ( "title" ) is None :
1696+ kwargs [ "title" ] = metadata [ "metadata" ][ "title" ]
1697+ if kwargs . get ( "title" ) is None :
16861698 raise InvalidNodeException (
1687- "No source_id was provided and the QTI file {} does not have an identifier " .format (
1688- qti_file .path
1699+ "No title was provided and the IMSCP file {} does not have a title " .format (
1700+ imscp_file .path
16891701 )
16901702 )
1691- self . title = self . title or get_by_path (
1692- metadata , "metadata" , "general" , "title" , "string"
1693- )
1694- if self . title is None :
1703+
1704+ if "identifier" in metadata and kwargs . get ( "source_id" ) is None :
1705+ kwargs [ "source_id" ] = metadata [ "identifier" ]
1706+ if kwargs . get ( "source_id" ) is None :
16951707 raise InvalidNodeException (
1696- "No title was provided and the QTI file {} does not have a title " .format (
1697- qti_file .path
1708+ "No source_id was provided and the IMSCP file {} does not have an identifier " .format (
1709+ imscp_file .path
16981710 )
16991711 )
1712+ license = kwargs .pop ("license" )
1713+ if license is None :
1714+ raise InvalidNodeException (
1715+ "No license was provided and we cannot infer license from an IMSCP file"
1716+ )
1717+ if metadata .get ("organizations" ):
1718+ node = TopicNode (* args , ** kwargs )
1719+
1720+ for child in metadata ["organizations" ]:
1721+ cls ._recurse_and_add_children (node , child , license )
1722+ else :
1723+ node = ContentNode (* args , ** kwargs )
1724+ node .kind = cls .kind
1725+ update_node_from_metadata (node , metadata ["metadata" ])
1726+ return node
1727+
1728+
1729+ class QTINode (IMSCPNode ):
1730+ """
1731+ Node representing QTI exercise
1732+ """
1733+
1734+ kind = content_kinds .EXERCISE
17001735
17011736 def validate (self ):
17021737 """validate: Makes sure QTI is valid
0 commit comments