Skip to content

Commit 42c7861

Browse files
committed
Do recursive tree generation for IMSCP nodes.
Write basic tests to confirm behaviour.
1 parent e7d5aef commit 42c7861

File tree

3 files changed

+252
-168
lines changed

3 files changed

+252
-168
lines changed

ricecooker/classes/nodes.py

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ..exceptions import InvalidNodeException
2222
from ..utils.utils import is_valid_uuid_string
2323
from .licenses import License
24+
from ricecooker.utils.SCORM_metadata import update_node_from_metadata
2425

2526
MASTERY_MODELS = [id for id, name in exercises.MASTERY_MODELS]
2627
ROLES = [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):
16491651
RemoteContentNode = 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

Comments
 (0)