55If student does not yet answered - `num_inputs` numbers of text inputs.
66If student have answered - words he entered and cloud.
77"""
8+ import copy
9+ import json
10+ import logging
11+ import os
812import uuid
913
1014from django .utils .translation import gettext_noop as _
15+ from lxml import etree
16+ from lxml .etree import ElementTree
1117from web_fragments .fragment import Fragment
12- from xblock .core import XBlock
13- from xblock .fields import Boolean , Dict , Integer , List , Scope , String
18+ from xblock .core import XML_NAMESPACES , XBlock
19+ from xblock .fields import Boolean , Dict , Integer , List , Scope , ScopeIds , String
1420from xblock .utils .resources import ResourceLoader
1521from xblock .utils .studio_editable import StudioEditableXBlockMixin
1622
23+ from xblocks_contrib .common .xml_utils import (
24+ apply_pointer_attributes ,
25+ deserialize_field ,
26+ format_filepath ,
27+ is_pointer_tag ,
28+ load_definition_xml ,
29+ name_to_pathname ,
30+ own_metadata ,
31+ serialize_field ,
32+ )
33+
1734resource_loader = ResourceLoader (__name__ )
35+ log = logging .getLogger (__name__ )
1836
1937
2038def pretty_bool (value ):
@@ -27,8 +45,270 @@ def pretty_bool(value):
2745 return value in bool_dict
2846
2947
48+ class WordCloudXmlMixin :
49+ """
50+ A mixin class to add XML parsing functionality to WordCloud.
51+ """
52+
53+ metadata_to_strip = (
54+ 'data_dir' ,
55+ 'tabs' ,
56+ 'grading_policy' ,
57+ 'discussion_blackouts' ,
58+ 'course' ,
59+ 'org' ,
60+ 'url_name' ,
61+ 'filename' ,
62+ 'xml_attributes' ,
63+ "x-is-pointer-node" ,
64+ )
65+
66+ xml_attributes = Dict (
67+ help = "Map of unhandled xml attributes, used only for storage between import and export" ,
68+ default = {},
69+ scope = Scope .settings
70+ )
71+
72+ @classmethod
73+ def apply_policy (cls , metadata , policy ):
74+ """
75+ Add the keys in policy to metadata, after processing them
76+ through the attrmap. Updates the metadata dict in place.
77+ """
78+ for attr , value in policy .items ():
79+ if attr not in cls .fields :
80+ # Store unknown attributes coming from policy.json
81+ # in such a way that they will export to xml unchanged
82+ metadata ['xml_attributes' ][attr ] = value
83+ else :
84+ metadata [attr ] = value
85+
86+ @staticmethod
87+ def _get_metadata_from_xml (xml_object , remove = True ):
88+ """
89+ Extract the metadata from the XML.
90+ """
91+ meta = xml_object .find ('meta' )
92+ if meta is None :
93+ return ''
94+ dmdata = meta .text
95+ if remove :
96+ xml_object .remove (meta )
97+ return dmdata
98+
99+ @classmethod
100+ def clean_metadata_from_xml (cls , xml_object , excluded_fields = ()):
101+ """
102+ Remove any attribute named for a field with scope Scope.settings from the supplied
103+ xml_object
104+ """
105+ for field_name , field in cls .fields .items ():
106+ if (field .scope == Scope .settings
107+ and field_name not in excluded_fields
108+ and xml_object .get (field_name ) is not None ):
109+ del xml_object .attrib [field_name ]
110+
111+ @classmethod
112+ def load_definition (cls , xml_object ):
113+ """
114+ Load a block from the specified xml_object.
115+
116+ Args:
117+ xml_object: an lxml.etree._Element containing the definition to load
118+ """
119+
120+ filename = xml_object .get ('filename' )
121+ definition_xml = copy .deepcopy (xml_object )
122+ filepath = ''
123+
124+ definition_metadata = cls ._get_metadata_from_xml (definition_xml )
125+ cls .clean_metadata_from_xml (definition_xml )
126+
127+ if len (xml_object ) == 0 and len (list (xml_object .items ())) == 0 :
128+ definition , children = {'data' : '' }, []
129+ else :
130+ definition , children = {'data' : etree .tostring (xml_object , pretty_print = True , encoding = 'unicode' )}, []
131+
132+ if definition_metadata :
133+ definition ['definition_metadata' ] = definition_metadata
134+ definition ['filename' ] = [filepath , filename ]
135+
136+ return definition , children
137+
138+ @classmethod
139+ def load_metadata (cls , xml_object ):
140+ """
141+ Read the metadata attributes from this xml_object.
142+
143+ Returns a dictionary {key: value}.
144+ """
145+ metadata = {'xml_attributes' : {}}
146+ for attr , val in xml_object .attrib .items ():
147+
148+ if attr in cls .metadata_to_strip :
149+ # don't load these
150+ continue
151+
152+ if attr not in cls .fields :
153+ metadata ['xml_attributes' ][attr ] = val
154+ else :
155+ metadata [attr ] = deserialize_field (cls .fields [attr ], val )
156+ return metadata
157+
158+ @classmethod
159+ def parse_xml (cls , node , runtime , keys ):
160+ """
161+ Use `node` to construct a new block.
162+
163+ Arguments:
164+ node (etree.Element): The xml node to parse into an xblock.
165+
166+ runtime (:class:`.Runtime`): The runtime to use while parsing.
167+
168+ keys (:class:`.ScopeIds`): The keys identifying where this block
169+ will store its data.
170+
171+ Returns (XBlock): The newly parsed XBlock
172+
173+ """
174+ if keys is None :
175+ # Passing keys=None is against the XBlock API but some platform tests do it.
176+ def_id = runtime .id_generator .create_definition (node .tag , node .get ("url_name" ))
177+ keys = ScopeIds (None , node .tag , def_id , runtime .id_generator .create_usage (def_id ))
178+ aside_children = []
179+
180+ # Let the runtime construct the block. It will have a proper, inheritance-aware field data store.
181+ block = runtime .construct_xblock_from_class (cls , keys )
182+
183+ # VS[compat]
184+ # In 2012, when the platform didn't have CMS, and all courses were handwritten XML files, problem tags
185+ # contained XML problem descriptions withing themselves. Later, when Studio has been created, and "pointer" tags
186+ # became the preferred problem format, edX has to add this compatibility code to 1) support both pre- and
187+ # post-Studio course formats simulteneously, and 2) be able to migrate 2012-fall courses to Studio. Old style
188+ # support supposed to be removed, but the deprecation process have never been initiated, so this
189+ # compatibility must stay, probably forever.
190+ if is_pointer_tag (node ):
191+ # new style:
192+ # read the actual definition file--named using url_name.replace(':','/')
193+ definition_xml , filepath = load_definition_xml (node , runtime , keys .def_id )
194+ aside_children = runtime .parse_asides (definition_xml , keys .def_id , keys .usage_id , runtime .id_generator )
195+ else :
196+ filepath = None
197+ definition_xml = node
198+
199+ # Removes metadata
200+ definition , children = cls .load_definition (definition_xml )
201+
202+ # VS[compat]
203+ # Make Ike's github preview links work in both old and new file layouts.
204+ if is_pointer_tag (node ):
205+ # new style -- contents actually at filepath
206+ definition ["filename" ] = [filepath , filepath ]
207+
208+ metadata = cls .load_metadata (definition_xml )
209+
210+ # move definition metadata into dict
211+ dmdata = definition .get ("definition_metadata" , "" )
212+ if dmdata :
213+ metadata ["definition_metadata_raw" ] = dmdata
214+ try :
215+ metadata .update (json .loads (dmdata ))
216+ except Exception as err : # lint-amnesty, pylint: disable=broad-except
217+ log .debug ("Error in loading metadata %r" , dmdata , exc_info = True )
218+ metadata ["definition_metadata_err" ] = str (err )
219+
220+ definition_aside_children = definition .pop ("aside_children" , None )
221+ if definition_aside_children :
222+ aside_children .extend (definition_aside_children )
223+
224+ # Set/override any metadata specified by policy
225+ cls .apply_policy (metadata , runtime .get_policy (keys .usage_id ))
226+
227+ field_data = {** metadata , ** definition }
228+
229+ for field_name , value in field_data .items ():
230+ # The 'xml_attributes' field has a special setter logic in its Field class,
231+ # so we must handle it carefully to avoid duplicating data.
232+ if field_name == "xml_attributes" :
233+ # The 'filename' attribute is specially handled for git links.
234+ value ["filename" ] = definition .get ("filename" , ["" , None ])
235+ block .xml_attributes .update (value )
236+ elif field_name in block .fields :
237+ setattr (block , field_name , value )
238+
239+ block .children = children
240+
241+ if aside_children :
242+ cls .add_applicable_asides_to_block (block , runtime , aside_children )
243+
244+ return block
245+
246+ @classmethod
247+ def add_applicable_asides_to_block (cls , block , runtime , aside_children ):
248+ """
249+ Add asides to the block. Moved this out of the parse_xml method to use it in the VideoBlock.parse_xml
250+ """
251+ asides_tags = [aside_child .tag for aside_child in aside_children ]
252+ asides = runtime .get_asides (block )
253+ for aside in asides :
254+ if aside .scope_ids .block_type in asides_tags :
255+ block .add_aside (aside )
256+
257+ def export_to_file (self ):
258+ """If this returns True, write the definition of this block to a separate
259+ file.
260+ """
261+ return True
262+
263+ def add_xml_to_node (self , node ):
264+ """For exporting, set data on `node` from ourselves."""
265+ xml_object = etree .Element (self .category )
266+
267+ if xml_object is None :
268+ return
269+
270+ for aside in self .runtime .get_asides (self ):
271+ if aside .needs_serialization ():
272+ aside_node = etree .Element ("unknown_root" , nsmap = XML_NAMESPACES )
273+ aside .add_xml_to_node (aside_node )
274+ xml_object .append (aside_node )
275+
276+ self .clean_metadata_from_xml (xml_object )
277+ xml_object .tag = self .category
278+ node .tag = self .category
279+
280+ for attr in sorted (own_metadata (self )):
281+ if attr not in self .metadata_to_strip :
282+ # pylint: disable=unsubscriptable-object
283+ val = serialize_field (self .fields [attr ].to_json (getattr (self , attr )))
284+ try :
285+ xml_object .set (attr , val )
286+ except Exception : # pylint: disable=broad-exception-caught
287+ logging .exception ("Failed to serialize metadata attribute %s in module %s." , attr , self .url_name )
288+
289+ for key , value in self .xml_attributes .items ():
290+ if key not in self .metadata_to_strip :
291+ xml_object .set (key , serialize_field (value ))
292+
293+ if self .export_to_file ():
294+ url_path = name_to_pathname (self .url_name )
295+ filepath = format_filepath (self .category , url_path )
296+ self .runtime .export_fs .makedirs (os .path .dirname (filepath ), recreate = True )
297+ with self .runtime .export_fs .open (filepath , "wb" ) as fileobj :
298+ ElementTree (xml_object ).write (fileobj , pretty_print = True , encoding = "utf-8" )
299+ else :
300+ node .clear ()
301+ node .tag = xml_object .tag
302+ node .text = xml_object .text
303+ node .tail = xml_object .tail
304+ node .attrib .update (xml_object .attrib )
305+ node .extend (xml_object )
306+
307+ apply_pointer_attributes (node , self )
308+
309+
30310@XBlock .needs ("i18n" )
31- class WordCloudBlock (StudioEditableXBlockMixin , XBlock ):
311+ class WordCloudBlock (StudioEditableXBlockMixin , WordCloudXmlMixin , XBlock ):
32312 """
33313 Word Cloud XBlock.
34314 """
0 commit comments