44"""
55
66import copy
7- import datetime
87import json
98import logging
109import os
1413from html .parser import HTMLParser
1514
1615from django .conf import settings
17- from django .core .serializers .json import DjangoJSONEncoder
1816from django .utils .translation import gettext_noop as _
1917from fs .errors import ResourceNotFound
2018from lxml import etree
2725from xblock .fields import Boolean , Dict , Scope , ScopeIds , String , UserScope
2826from xblock .utils .resources import ResourceLoader
2927
28+ from xblocks_contrib .common .xml_utils import (
29+ apply_pointer_attributes ,
30+ deserialize_field ,
31+ format_filepath ,
32+ is_pointer_tag ,
33+ load_definition_xml ,
34+ name_to_pathname ,
35+ own_metadata ,
36+ serialize_field ,
37+ )
38+
3039log = logging .getLogger (__name__ )
3140resource_loader = ResourceLoader (__name__ )
3241
3746EDX_XML_PARSER = XMLParser (dtd_validation = False , load_dtd = False , remove_blank_text = True , encoding = "utf-8" )
3847
3948
40- class EdxJSONEncoder (DjangoJSONEncoder ):
41- """
42- Custom JSONEncoder that handles ``Location`` and ``datetime.datetime`` objects.
43- Encodes ``Location`` as its URL string form, and ``datetime.datetime`` as an ISO 8601 string.
44- """
45-
46- def default (self , o ):
47- if isinstance (o , (CourseKey , UsageKey )):
48- return str (o )
49- elif isinstance (o , datetime .datetime ):
50- if o .tzinfo is not None :
51- if o .utcoffset () is None :
52- return o .isoformat () + "Z"
53- else :
54- return o .isoformat ()
55- else :
56- return o .isoformat ()
57- else :
58- return super ().default (o )
59-
60-
6149class MLStripper (HTMLParser ):
6250 "helper function for html_to_text below"
6351
@@ -164,95 +152,6 @@ def stringify_children(node):
164152 return "" .join ([part for part in parts if part ])
165153
166154
167- def name_to_pathname (name ):
168- """
169- Convert a location name for use in a path: replace ':' with '/'.
170- This allows users of the xml format to organize content into directories
171- """
172- return name .replace (":" , "/" )
173-
174-
175- def is_pointer_tag (xml_obj ):
176- """
177- Check if xml_obj is a pointer tag: <blah url_name="something" />.
178- No children, one attribute named url_name, no text.
179-
180- Special case for course roots: the pointer is
181- <course url_name="something" org="myorg" course="course">
182-
183- xml_obj: an etree Element
184-
185- Returns a bool.
186- """
187- if xml_obj .tag != "course" :
188- expected_attr = {"url_name" }
189- else :
190- expected_attr = {"url_name" , "course" , "org" }
191-
192- actual_attr = set (xml_obj .attrib .keys ())
193-
194- has_text = xml_obj .text is not None and len (xml_obj .text .strip ()) > 0
195-
196- return len (xml_obj ) == 0 and actual_attr == expected_attr and not has_text
197-
198-
199- def serialize_field (value ):
200- """
201- Return a string version of the value (where value is the JSON-formatted, internally stored value).
202-
203- If the value is a string, then we simply return what was passed in.
204- Otherwise, we return json.dumps on the input value.
205- """
206- if isinstance (value , str ):
207- return value
208- elif isinstance (value , datetime .datetime ):
209- if value .tzinfo is not None and value .utcoffset () is None :
210- return value .isoformat () + "Z"
211- return value .isoformat ()
212-
213- return json .dumps (value , cls = EdxJSONEncoder )
214-
215-
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-
248- def own_metadata (block ):
249- """
250- Return a JSON-friendly dictionary that contains only non-inherited field
251- keys, mapped to their serialized values
252- """
253- return block .get_explicitly_set_fields_by_scope (Scope .settings )
254-
255-
256155@XBlock .needs ("i18n" )
257156# We 'want' the user service, but we don't strictly 'need' it.
258157# This makes our block more resilient. It won't crash in test environments
@@ -600,31 +499,6 @@ def clean_metadata_from_xml(cls, xml_object, excluded_fields=()):
600499 ):
601500 del xml_object .attrib [field_name ]
602501
603- @classmethod
604- def file_to_xml (cls , file_object ):
605- """
606- Used when this module wants to parse a file object to xml
607- that will be converted to the definition.
608-
609- Returns an lxml Element
610- """
611- return etree .parse (file_object , parser = EDX_XML_PARSER ).getroot () # CHANGEE
612-
613- @classmethod
614- def load_file (cls , filepath , fs , def_id ):
615- """
616- Open the specified file in fs, and call cls.file_to_xml on it,
617- returning the lxml object.
618-
619- Add details and reraise on error.
620- """
621- try :
622- with fs .open (filepath ) as xml_file :
623- return cls .file_to_xml (xml_file )
624- except Exception as err :
625- # Add info about where we are, but keep the traceback
626- raise Exception (f"Unable to load file contents at path { filepath } for item { def_id } : { err } " ) from err
627-
628502 # NOTE: html descriptors are special. We do not want to parse and
629503 # export them ourselves, because that can break things (e.g. lxml
630504 # adds body tags when it exports, but they should just be html
@@ -769,7 +643,7 @@ def parse_xml(cls, node, runtime, keys):
769643 if is_pointer_tag (node ):
770644 # new style:
771645 # read the actual definition file--named using url_name.replace(':','/')
772- definition_xml , filepath = cls . load_definition_xml (node , runtime , keys .def_id )
646+ definition_xml , filepath = load_definition_xml (node , runtime , keys .def_id )
773647 aside_children = runtime .parse_asides (definition_xml , keys .def_id , keys .usage_id , runtime .id_generator )
774648 else :
775649 filepath = None
@@ -847,21 +721,6 @@ def parse_xml_new_runtime(cls, node, runtime, keys):
847721 cls ._set_field_if_present (block , name , value , {})
848722 return block
849723
850- @classmethod
851- def load_definition_xml (cls , node , runtime , def_id ):
852- """
853- Loads definition_xml stored in a dedicated file
854- """
855- url_name = node .get ("url_name" )
856- filepath = cls ._format_filepath (node .tag , name_to_pathname (url_name ))
857- definition_xml = cls .load_file (filepath , runtime .resources_fs , def_id )
858- return definition_xml , filepath
859-
860- @classmethod
861- def _format_filepath (cls , category , name ):
862- """Formats a path to an XML definition file."""
863- return f"{ category } /{ name } .{ cls .filename_extension } "
864-
865724 def export_to_file (self ):
866725 """If this returns True, write the definition of this block to a separate
867726 file.
@@ -907,7 +766,7 @@ def add_xml_to_node(self, node):
907766
908767 if self .export_to_file ():
909768 url_path = name_to_pathname (self .url_name )
910- filepath = self . _format_filepath (
769+ filepath = format_filepath (
911770 self .category , self .location .run if self .category == "course" else url_path
912771 )
913772 self .runtime .export_fs .makedirs (os .path .dirname (filepath ), recreate = True )
@@ -921,12 +780,7 @@ def add_xml_to_node(self, node):
921780 node .attrib .update (xml_object .attrib )
922781 node .extend (xml_object )
923782
924- if not node .get ("url_name" ):
925- node .set ("url_name" , self .url_name )
926-
927- if self .category == "course" :
928- node .set ("org" , self .location .org )
929- node .set ("course" , self .location .course )
783+ apply_pointer_attributes (node , self )
930784
931785 def definition_to_xml (self , resource_fs ):
932786 """
0 commit comments