Skip to content

Commit 0fee77c

Browse files
committed
fix: tests
1 parent adefe4d commit 0fee77c

File tree

1 file changed

+134
-45
lines changed

1 file changed

+134
-45
lines changed

xblocks_contrib/html/html.py

Lines changed: 134 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from path import Path as path
2525
from web_fragments.fragment import Fragment
2626
from xblock.core import XML_NAMESPACES, XBlock
27-
from xblock.fields import Boolean, Dict, List, Scope, String, UserScope
27+
from xblock.fields import Boolean, Dict, Scope, ScopeIds, String, UserScope
2828
from xblock.utils.resources import ResourceLoader
2929

3030
log = logging.getLogger(__name__)
@@ -213,6 +213,38 @@ def serialize_field(value):
213213
return json.dumps(value, cls=EdxJSONEncoder)
214214

215215

216+
def deserialize_field(field, value):
217+
"""
218+
Deserialize the string version to the value stored internally.
219+
220+
Note that this is not the same as the value returned by from_json, as model types typically store
221+
their value internally as JSON. By default, this method will return the result of calling json.loads
222+
on the supplied value, unless json.loads throws a TypeError, or the type of the value returned by json.loads
223+
is not supported for this class (from_json throws an Error). In either of those cases, this method returns
224+
the input value.
225+
"""
226+
try:
227+
deserialized = json.loads(value)
228+
if deserialized is None:
229+
return deserialized
230+
try:
231+
field.from_json(deserialized)
232+
return deserialized
233+
except (ValueError, TypeError):
234+
# Support older serialized version, which was just a string, not result of json.dumps.
235+
# If the deserialized version cannot be converted to the type (via from_json),
236+
# just return the original value. For example, if a string value of '3.4' was
237+
# stored for a String field (before we started storing the result of json.dumps),
238+
# then it would be deserialized as 3.4, but 3.4 is not supported for a String
239+
# field. Therefore field.from_json(3.4) will throw an Error, and we should
240+
# actually return the original value of '3.4'.
241+
return value
242+
243+
except (ValueError, TypeError):
244+
# Support older serialized version.
245+
return value
246+
247+
216248
def own_metadata(block):
217249
"""
218250
Return a JSON-friendly dictionary that contains only non-inherited field
@@ -298,18 +330,6 @@ class HtmlBlock(XBlock):
298330

299331
metadata_to_export_to_policy = ("discussion_topics",)
300332

301-
children = List(
302-
help=_("Child blocks of this XBlock."),
303-
default=[],
304-
scope=Scope.content,
305-
)
306-
307-
url_name = String(
308-
help=_("Unique URL-friendly identifier of the block."),
309-
scope=Scope.settings,
310-
default="",
311-
)
312-
313333
@property
314334
def category(self):
315335
return self.scope_ids.block_type
@@ -327,7 +347,7 @@ def location(self, value):
327347
)
328348

329349
@property
330-
def _url_name(self):
350+
def url_name(self):
331351
return self.location.block_id
332352

333353
@property
@@ -669,6 +689,40 @@ def load_definition(cls, xml_object, system, location, id_generator): # pylint:
669689
# add more info and re-raise
670690
raise Exception(msg).with_traceback(sys.exc_info()[2])
671691

692+
@classmethod
693+
def load_metadata(cls, xml_object):
694+
"""
695+
Read the metadata attributes from this xml_object.
696+
697+
Returns a dictionary {key: value}.
698+
"""
699+
metadata = {"xml_attributes": {}}
700+
for attr, val in xml_object.attrib.items():
701+
702+
if attr in cls.metadata_to_strip:
703+
# don't load these
704+
continue
705+
706+
if attr not in cls.fields: # pylint: disable=unsupported-membership-test
707+
metadata["xml_attributes"][attr] = val
708+
else:
709+
metadata[attr] = deserialize_field(cls.fields[attr], val) # pylint: disable=unsubscriptable-object
710+
return metadata
711+
712+
@classmethod
713+
def apply_policy(cls, metadata, policy):
714+
"""
715+
Add the keys in policy to metadata, after processing them
716+
through the attrmap. Updates the metadata dict in place.
717+
"""
718+
for attr, value in policy.items():
719+
if attr not in cls.fields: # pylint: disable=unsupported-membership-test
720+
# Store unknown attributes coming from policy.json
721+
# in such a way that they will export to xml unchanged
722+
metadata["xml_attributes"][attr] = value
723+
else:
724+
metadata[attr] = value
725+
672726
@classmethod
673727
def parse_xml(cls, node, runtime, keys):
674728
"""
@@ -686,6 +740,15 @@ def parse_xml(cls, node, runtime, keys):
686740
687741
"""
688742

743+
if keys is None:
744+
# Passing keys=None is against the XBlock API but some platform tests do it.
745+
def_id = runtime.id_generator.create_definition(node.tag, node.get("url_name"))
746+
keys = ScopeIds(None, node.tag, def_id, runtime.id_generator.create_usage(def_id))
747+
aside_children = []
748+
749+
# Let the runtime construct the block. It will have a proper, inheritance-aware field data store.
750+
block = runtime.construct_xblock_from_class(cls, keys)
751+
689752
# VS[compat]
690753
# In 2012, when the platform didn't have CMS, and all courses were handwritten XML files, problem tags
691754
# contained XML problem descriptions withing themselves. Later, when Studio has been created, and "pointer" tags
@@ -696,32 +759,70 @@ def parse_xml(cls, node, runtime, keys):
696759
if is_pointer_tag(node):
697760
# new style:
698761
# read the actual definition file--named using url_name.replace(':','/')
699-
definition_xml, _ = cls.load_definition_xml(node, runtime, keys.def_id)
762+
definition_xml, filepath = cls.load_definition_xml(node, runtime, keys.def_id)
763+
aside_children = runtime.parse_asides(definition_xml, keys.def_id, keys.usage_id, runtime.id_generator)
700764
else:
765+
filepath = None
701766
definition_xml = node
702767

703-
block = runtime.construct_xblock_from_class(cls, keys)
768+
# Note: removes metadata.
769+
definition, children = cls.load_definition(definition_xml, runtime, keys.def_id, runtime.id_generator)
770+
771+
# VS[compat]
772+
# Make Ike's github preview links work in both old and new file layouts.
773+
if is_pointer_tag(node):
774+
# new style -- contents actually at filepath
775+
definition["filename"] = [filepath, filepath]
776+
777+
metadata = cls.load_metadata(definition_xml)
778+
779+
# move definition metadata into dict
780+
dmdata = definition.get("definition_metadata", "")
781+
if dmdata:
782+
metadata["definition_metadata_raw"] = dmdata
783+
try:
784+
metadata.update(json.loads(dmdata))
785+
except Exception as err: # lint-amnesty, pylint: disable=broad-except
786+
log.debug("Error in loading metadata %r", dmdata, exc_info=True)
787+
metadata["definition_metadata_err"] = str(err)
788+
789+
definition_aside_children = definition.pop("aside_children", None)
790+
if definition_aside_children:
791+
aside_children.extend(definition_aside_children)
792+
793+
# Set/override any metadata specified by policy
794+
cls.apply_policy(metadata, runtime.get_policy(keys.usage_id))
795+
796+
field_data = {**metadata, **definition}
797+
798+
for field_name, value in field_data.items():
799+
# The 'xml_attributes' field has a special setter logic in its Field class,
800+
# so we must handle it carefully to avoid duplicating data.
801+
if field_name == "xml_attributes":
802+
# The 'filename' attribute is specially handled for git links.
803+
value["filename"] = definition.get("filename", ["", None])
804+
block.xml_attributes.update(value)
805+
elif field_name in block.fields:
806+
setattr(block, field_name, value)
807+
808+
block.children = children
704809

705-
for source_xml in [node, definition_xml]:
706-
for name, value in source_xml.items():
707-
if name in block.fields:
708-
field = block.fields[name]
709-
try:
710-
converted_value = field.from_string(value)
711-
setattr(block, name, converted_value)
712-
except Exception: # pylint: disable=broad-exception-caught
713-
log.warning("Could not set field '%s' from string value '%s'", name, value, exc_info=True)
714-
elif not hasattr(block, "xml_attributes") or name not in block.xml_attributes:
715-
if not hasattr(block, "xml_attributes"):
716-
block.xml_attributes = {}
717-
block.xml_attributes[name] = value
718-
719-
definition, _ = cls.load_definition(definition_xml, runtime, keys.def_id, runtime.id_generator)
720-
if "data" in definition:
721-
block.data = definition["data"]
810+
if aside_children:
811+
cls.add_applicable_asides_to_block(block, runtime, aside_children)
722812

723813
return block
724814

815+
@classmethod
816+
def add_applicable_asides_to_block(cls, block, runtime, aside_children):
817+
"""
818+
Add asides to the block. Moved this out of the parse_xml method to use it in the VideoBlock.parse_xml
819+
"""
820+
asides_tags = [aside_child.tag for aside_child in aside_children]
821+
asides = runtime.get_asides(block)
822+
for aside in asides:
823+
if aside.scope_ids.block_type in asides_tags:
824+
block.add_aside(aside)
825+
725826
@classmethod
726827
def parse_xml_new_runtime(cls, node, runtime, keys):
727828
"""
@@ -823,8 +924,6 @@ def definition_to_xml(self, resource_fs):
823924
This version creates a self-contained definition file that includes
824925
all necessary metadata for a successful re-import.
825926
"""
826-
if not self.url_name:
827-
self.url_name = self.location.block_id
828927

829928
# Write html to file, return an empty tag
830929
pathname = name_to_pathname(self.url_name)
@@ -840,16 +939,6 @@ def definition_to_xml(self, resource_fs):
840939

841940
elt = etree.Element("html")
842941
elt.set("filename", relname)
843-
844-
# This loop adds other metadata attributes from the block.
845-
for field_name in self.fields: # pylint: disable=not-an-iterable
846-
# pylint: disable=unsubscriptable-object
847-
if field_name not in ["data", "children", "parent"] and self.fields[field_name].is_set_on(self):
848-
field = self.fields[field_name] # pylint: disable=unsubscriptable-object
849-
value = field.read_from(self)
850-
if value is not None:
851-
elt.set(field_name, str(value))
852-
853942
return elt
854943

855944
@property

0 commit comments

Comments
 (0)