2424from path import Path as path
2525from web_fragments .fragment import Fragment
2626from 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
2828from xblock .utils .resources import ResourceLoader
2929
3030log = 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+
216248def 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