Skip to content

Commit c05669f

Browse files
committed
chore: add pointer tag OLX support for WordCloud XBlock
1 parent 3e10a9d commit c05669f

File tree

1 file changed

+283
-3
lines changed

1 file changed

+283
-3
lines changed

xblocks_contrib/word_cloud/word_cloud.py

Lines changed: 283 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,34 @@
55
If student does not yet answered - `num_inputs` numbers of text inputs.
66
If student have answered - words he entered and cloud.
77
"""
8+
import copy
9+
import json
10+
import logging
11+
import os
812
import uuid
913

1014
from django.utils.translation import gettext_noop as _
15+
from lxml import etree
16+
from lxml.etree import ElementTree
1117
from 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
1420
from xblock.utils.resources import ResourceLoader
1521
from 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+
1734
resource_loader = ResourceLoader(__name__)
35+
log = logging.getLogger(__name__)
1836

1937

2038
def 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

Comments
 (0)