Commit c813d548 by Braden MacDonald

Upgrade script to help with upgrading from v1 to v2 + tests

parent af43a26d
"""
The mentoring XBlock was previously designed to be edited using XML.
This file contains a hack necessary for us to parse the old XML data and create blocks in Studio
from the parsed XML, as part of the upgrade process.
It works by parsing the XML and creating XBlocks in a temporary runtime
environment, so that the blocks' fields can be read and copied into Studio.
"""
from xblock.fields import Scope
from xblock.runtime import Runtime, DictKeyValueStore, KvsFieldData, MemoryIdManager, ScopeIds
class TransientRuntime(Runtime):
"""
An XBlock runtime designed to have no persistence and no ability to render views/handlers.
"""
def __init__(self):
id_manager = MemoryIdManager()
field_data = KvsFieldData(DictKeyValueStore())
super(TransientRuntime, self).__init__(
id_reader=id_manager,
id_generator=id_manager,
field_data=field_data,
)
def create_block_from_node(self, node):
"""
Parse an XML node representing an XBlock (and children), and return the XBlock.
"""
block_type = node.tag
def_id = self.id_generator.create_definition(block_type)
usage_id = self.id_generator.create_usage(def_id)
keys = ScopeIds(None, block_type, def_id, usage_id)
block_class = self.mixologist.mix(self.load_block_type(block_type))
block = block_class.parse_xml(node, self, keys, self.id_generator)
block.save()
return block
def handler_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support handler_url.")
def local_resource_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support local_resource_url.")
def publish(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support publish.")
def resource_url(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime does not support resource_url.")
def render_template(self, *args, **kwargs):
raise NotImplementedError("TransientRuntime cannot render templates.")
def studio_update_from_node(block, node):
"""
Given an XBlock that is using the edX Studio runtime, replace all of block's fields and
children with the fields and children defined by the XML node 'node'.
"""
user_id = block.runtime.user_id
temp_runtime = TransientRuntime()
source_block = temp_runtime.create_block_from_node(node)
def update_from_temp_block(real_block, temp_block):
"""
Recursively copy all fields and children from temp_block to real_block.
"""
# Fields:
for field_name, field in temp_block.fields.iteritems():
if field.scope in (Scope.content, Scope.settings) and field.is_set_on(temp_block):
setattr(real_block, field_name, getattr(temp_block, field_name))
# Children:
if real_block.has_children:
real_block.children = []
for child_id in temp_block.children:
child = temp_block.runtime.get_block(child_id)
new_child = real_block.runtime.modulestore.create_item(
user_id, real_block.location.course_key, child.scope_ids.block_type
)
update_from_temp_block(new_child, child)
real_block.children.append(new_child.location)
real_block.save()
real_block.runtime.modulestore.update_item(real_block, user_id)
with block.runtime.modulestore.bulk_operations(block.location.course_key):
for child_id in block.children:
block.runtime.modulestore.delete_item(child_id, user_id)
update_from_temp_block(block, source_block)
"""
Test that we can upgrade from mentoring v1 to mentoring v2.
"""
import ddt
from lxml import etree
from mentoring.v1.xml_changes import convert_xml_v1_to_v2
import os.path
from StringIO import StringIO
import unittest
from sample_xblocks.basic.content import HtmlBlock
from xblock.core import XBlock
from xblock.fields import ScopeIds
from xblock.runtime import DictKeyValueStore, KvsFieldData
from xblock.test.tools import TestRuntime
xml_path = os.path.join(os.path.dirname(__file__), "xml")
@ddt.ddt
class TestUpgrade(unittest.TestCase):
"""
Test upgrade from mentoring v1 (which uses xml_content even in Studio) to v2.
We can't test the steps that depend on Studio, so we just test the XML conversion.
"""
def setUp(self):
self.runtime = TestRuntime(field_data=KvsFieldData(DictKeyValueStore()))
@ddt.data(
"v1_upgrade_a",
"v1_upgrade_b",
"v1_upgrade_c",
)
@XBlock.register_temp_plugin(HtmlBlock, "html")
def test_xml_upgrade(self, file_name):
"""
Convert a v1 mentoring block to v2 and then compare the resulting block to a
pre-converted one.
"""
with open("{}/{}_old.xml".format(xml_path, file_name)) as xmlfile:
temp_node = etree.parse(xmlfile).getroot()
old_block = self.create_block_from_node(temp_node)
parser = etree.XMLParser(remove_blank_text=True)
xml_root = etree.parse(StringIO(old_block.xml_content), parser=parser).getroot()
convert_xml_v1_to_v2(xml_root)
converted_block = self.create_block_from_node(xml_root)
with open("{}/{}_new.xml".format(xml_path, file_name)) as xmlfile:
temp_node = etree.parse(xmlfile).getroot()
new_block = self.create_block_from_node(temp_node)
try:
self.assertBlocksAreEquivalent(converted_block, new_block)
except AssertionError:
xml_result = etree.tostring(xml_root, pretty_print=True, encoding="UTF-8")
print("Converted XML:\n{}".format(xml_result))
raise
def create_block_from_node(self, node):
"""
Parse an XML node representing an XBlock (and children), and return the XBlock.
"""
block_type = node.tag
def_id = self.runtime.id_generator.create_definition(block_type)
usage_id = self.runtime.id_generator.create_usage(def_id)
keys = ScopeIds(None, block_type, def_id, usage_id)
block_class = self.runtime.mixologist.mix(self.runtime.load_block_type(block_type))
block = block_class.parse_xml(node, self.runtime, keys, self.runtime.id_generator)
block.save()
return block
def assertBlocksAreEquivalent(self, block1, block2):
"""
Compare two blocks for equivalence.
Borrowed from xblock.test.tools.blocks_are_equivalent but modified to use assertions.
"""
# The two blocks have to be the same class.
self.assertEqual(block1.__class__, block2.__class__)
# They have to have the same fields.
self.assertEqual(set(block1.fields), set(block2.fields))
# The data fields have to have the same values.
for field_name in block1.fields:
if field_name in ('parent', 'children'):
continue
if field_name == "content":
# Inner HTML/XML content may have varying whitespace which we don't care about:
self.assertEqual(
self.clean_html(getattr(block1, field_name)),
self.clean_html(getattr(block2, field_name))
)
else:
self.assertEqual(getattr(block1, field_name), getattr(block2, field_name))
# The children need to be equal.
self.assertEqual(block1.has_children, block2.has_children)
if block1.has_children:
self.assertEqual(len(block1.children), len(block2.children))
for child_id1, child_id2 in zip(block1.children, block2.children):
# Load up the actual children to see if they are equal.
child1 = block1.runtime.get_block(child_id1)
child2 = block2.runtime.get_block(child_id2)
self.assertBlocksAreEquivalent(child1, child2)
def clean_html(self, html_str):
"""
Standardize the given HTML string for a consistent comparison.
Assumes the HTML is valid XML.
"""
# We wrap it in <x></x> so that the given HTML string doesn't need a single root element.
parser = etree.XMLParser(remove_blank_text=True)
parsed = etree.parse(StringIO(u"<x>{}</x>".format(html_str)), parser=parser).getroot()
return etree.tostring(parsed, pretty_print=False, encoding="UTF-8")[3:-3]
<mentoring url_name="some_url_name" weight="1" mode="standard" display_name="Default Title">
<html>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</html>
<html>
<p>Please answer the questions below.</p>
</html>
<answer name="goal" question="What is your goal?"/>
<mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices="yes">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great!</tip>
<tip values="maybenot">Ah, damn.</tip>
<tip values="understand">
<div id="test-custom-html">Really?</div>
</tip>
</mcq>
<rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices="4,5">
<choice value="notwant">I don't want to rate it</choice>
<tip values="4,5">I love good grades.</tip>
<tip values="1,2,3">Will do better next time...</tip>
<tip values="notwant">Your loss!</tip>
</rating>
<mrq name="mrq_1_1" question="What do you like in this MRQ?" message="Thank you for answering!" required_choices="gracefulness,elegance,beauty">
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip values="gracefulness">This MRQ is indeed very graceful</tip>
<tip values="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip values="bugs">Nah, there isn't any!</tip>
</mrq>
<message type="completed">
<p>Congratulations!</p>
</message>
<message type="incomplete">
<p>Still some work to do...</p>
</message>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains the old version of mentoring_default.xml (using the v1 schema)
Changes from the original:
- a <shared-header> was added to test that migration.
- the display_name was removed to avoid the warning about overwriting a display_name
since v2 only supports display_name rather than separate "title" and "display_name"
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring url_name="some_url_name" weight="1" mode="standard">
<title>Default Title</title>
<shared-header>
<p>This paragraph is shared between <strong>all</strong> questions.</p>
</shared-header>
<html>
<p>Please answer the questions below.</p>
</html>
<answer name="goal">
<question>What is your goal?</question>
</answer>
<mcq name="mcq_1_1" type="choices">
<question>Do you like this MCQ?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great!</tip>
<tip reject="maybenot">Ah, damn.</tip>
<tip reject="understand"><html><div id="test-custom-html">Really?</div></html></tip>
</mcq>
<mcq name="mcq_1_2" type="rating" low="Not good at all" high="Extremely good">
<question>How much do you rate this MCQ?</question>
<choice value="notwant">I don't want to rate it</choice>
<tip display="4,5">I love good grades.</tip>
<tip reject="1,2,3">Will do better next time...</tip>
<tip reject="notwant">Your loss!</tip>
</mcq>
<mrq name="mrq_1_1" type="choices">
<question>What do you like in this MRQ?</question>
<choice value="elegance">Its elegance</choice>
<choice value="beauty">Its beauty</choice>
<choice value="gracefulness">Its gracefulness</choice>
<choice value="bugs">Its bugs</choice>
<tip require="gracefulness">This MRQ is indeed very graceful</tip>
<tip require="elegance,beauty">This is something everyone has to like about this MRQ</tip>
<tip reject="bugs">Nah, there isn't any!</tip>
<message type="on-submit">Thank you for answering!</message>
</mrq>
<message type="completed">
<html><p>Congratulations!</p></html>
</message>
<message type="incomplete">
<html><p>Still some work to do...</p></html>
</message>
</mentoring>
]]>
</option:xml_content>
</mentoring>
<mentoring enforce_dependency="false" followed_by="past_attempts">
<html>
<h3>Checking your improvement frog</h3>
<p>Now, let's make sure your frog meets the criteria for a strong column 1. Here is your frog:</p>
</html>
<answer-recap name="improvement-frog"/>
<mcq name="frog-happy" question="Is this frog happy for you?" correct_choices="yes,maybenot,understand">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great. Your frog should be happy for you.</tip>
<tip values="maybenot">In the end, all the feedback you have gotten from others should not lead you to choose a frog that does not also feel happy and important to you.</tip>
<tip values="understand">
<p>If a frog is <span class="italic">happy for you</span>, that means it is a frog that you genuinely feel in your own heart to be something that you want to improve. What is in your heart?</p>
</tip>
</mcq>
<mcq name="frog-implicate" question="Does this frog implicate you?" correct_choices="yes,maybenot,understand">
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip values="yes">Great. Your frog should implicate you.</tip>
<tip values="maybenot">
<p>Since the Trial of Uruk-Shan focuses on your own growth and change, it is important to be clear about the ways <span class="bold">you</span> are hoping to change and improve.</p>
</tip>
<tip values="understand">Your frog implicates you if it is clear that you must get better at something. Your frog should focus on something you can control.</tip>
</mcq>
<rating name="frog-important" low="Not at all important to me" high="Very important to me" question="How important is it to you?" correct_choices="4,5,1,2,3,understand">
<choice value="understand">I don't understand</choice>
<tip values="4,5">Great!</tip>
<tip values="1,2,3">The Trial of Uruk-Shan helps you uncover some of the core beliefs and assumptions you have held that are preventing you from making change.</tip>
<tip values="understand">A frog is important if it is one that could make a big difference in helping you reach your frogs in your work life or your personal life (or both).</tip>
</rating>
<message type="completed">
Great! You have indicated that you have chosen a frog that is happy for you, implicates you, has room for improvement, and is important to you. You are now ready to move onto the next step.
</message>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains a typical problem taken from a live course (content changed)
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring enforce_dependency="false" followed_by="past_attempts">
<html>
<h3>Checking your improvement frog</h3>
<p>Now, let's make sure your frog meets the criteria for a strong column 1. Here is your frog:</p>
</html>
<answer name="improvement-frog" read_only="true"/>
<quizz name="frog-happy" type="choices">
<question>Is this frog happy for you?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great. Your frog should be happy for you.</tip>
<tip display="maybenot">In the end, all the feedback you have gotten from others should not lead you to choose a frog that does not also feel happy and important to you.</tip>
<tip display="understand">
<html>
<p>If a frog is <span class="italic">happy for you</span>, that means it is a frog that you genuinely feel in your own heart to be something that you want to improve. What is in your heart?</p>
</html>
</tip>
</quizz>
<quizz name="frog-implicate" type="choices">
<question>Does this frog implicate you?</question>
<choice value="yes">Yes</choice>
<choice value="maybenot">Maybe not</choice>
<choice value="understand">I don't understand</choice>
<tip display="yes">Great. Your frog should implicate you.</tip>
<tip display="maybenot">
<html>
<p>Since the Trial of Uruk-Shan focuses on your own growth and change, it is important to be clear about the ways <span class="bold">you</span> are hoping to change and improve.</p>
</html>
</tip>
<tip display="understand">Your frog implicates you if it is clear that you must get better at something. Your frog should focus on something you can control.</tip>
</quizz>
<quizz name="frog-important" type="rating" low="Not at all important to me" high="Very important to me">
<question>How important is it to you?</question>
<choice value="understand">I don't understand</choice>
<tip display="4,5">Great!</tip>
<tip display="1,2,3">The Trial of Uruk-Shan helps you uncover some of the core beliefs and assumptions you have held that are preventing you from making change.</tip>
<tip display="understand">A frog is important if it is one that could make a big difference in helping you reach your frogs in your work life or your personal life (or both).</tip>
</quizz>
<message type="completed">
Great! You have indicated that you have chosen a frog that is happy for you, implicates you, has room for improvement, and is important to you. You are now ready to move onto the next step.
</message>
</mentoring>
]]>
</option:xml_content>
</mentoring>
<mentoring display_submit="false" enforce_dependency="false">
<mentoring-table type="table_test" url_name="table_2">
<column header="Header Test 1">
<answer-recap name="table_1_answer_1" />
</column>
<column header="Header &lt;strong&gt;Test 2&lt;/strong&gt;">
<answer-recap name="table_1_answer_2" />
<html><p>Inline HTML</p></html>
<answer-recap name="table_1_answer_2" />
</column>
</mentoring-table>
</mentoring>
<?xml version='1.0' encoding='utf-8'?>
<!--
This contains a table to test migration of tables from v1 schema to v2.
-->
<mentoring xmlns:option="http://code.edx.org/xblock/option">
<option:xml_content>
<![CDATA[
<mentoring display_submit="false" enforce_dependency="false">
<mentoring-table type="table_test" url_name="table_2">
<column>
<header>Header Test 1</header>
<answer name="table_1_answer_1" />
</column>
<column>
<header><html>Header <strong>Test 2</strong></html></header>
<answer name="table_1_answer_2" />
<html><p>Inline HTML</p></html>
<answer name="table_1_answer_2" />
</column>
</mentoring-table>
</mentoring>
]]>
</option:xml_content>
</mentoring>
# -*- coding: utf-8 -*-
"""
The mentoring XBlock was previously designed to be edited using XML.
This file contains a script to help migrate mentoring blocks to the new format which is
optimized for editing in Studio.
To run the script on devstack:
SERVICE_VARIANT=cms DJANGO_SETTINGS_MODULE="cms.envs.devstack" python -m mentoring.v1.upgrade
"""
import logging
from lxml import etree
from mentoring import MentoringBlock
from StringIO import StringIO
import sys
from .studio_xml_utils import studio_update_from_node
from .xml_changes import convert_xml_v1_to_v2
def upgrade_block(block):
"""
Given a MentoringBlock "block" with old-style (v1) data in its "xml_content" field, parse
the XML and re-create the block with new-style (v2) children and settings.
"""
assert isinstance(block, MentoringBlock)
assert bool(block.xml_content) # If it's a v1 block it will have xml_content
xml_content_str = block.xml_content
parser = etree.XMLParser(remove_blank_text=True)
root = etree.parse(StringIO(xml_content_str), parser=parser).getroot()
assert root.tag == "mentoring"
convert_xml_v1_to_v2(root)
# We need some special-case handling to deal with HTML being an XModule and not a pure XBlock:
try:
from xmodule.html_module import HtmlDescriptor
except ImportError:
pass # Perhaps HtmlModule has been converted to an XBlock?
else:
@classmethod
def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator):
block = runtime.construct_xblock_from_class(cls, keys)
block.data = node.text if node.text else ""
for child in list(node):
if isinstance(child.tag, basestring):
block.data += etree.tostring(child)
return block
HtmlDescriptor.parse_xml = parse_xml_for_HtmlDescriptor
# Save the xml_content to make this processes rerunnable, in case it doesn't work correctly the first time.
root.attrib["xml_content"] = xml_content_str
# Replace block with the new version and the new children:
studio_update_from_node(block, root)
if __name__ == '__main__':
# Disable some distracting overly-verbose warnings that we don't need:
for noisy_module in ('edx.modulestore', 'elasticsearch', 'urllib3.connectionpool'):
logging.getLogger(noisy_module).setLevel(logging.ERROR)
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
print("┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
print("┃ Mentoring Upgrade Script ┃")
print("┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
try:
course_id = sys.argv[1]
except IndexError:
sys.exit("Need a course ID argument like 'HarvardX/GSE1.1x/3T2014' or 'course-v1:HarvardX+B101+2015'")
store = modulestore()
course = store.get_course(CourseKey.from_string(course_id))
print(" ➔ Found course: {}".format(course.display_name))
print(" ➔ Searching for mentoring blocks")
blocks_found = []
def find_mentoring_blocks(block):
if isinstance(block, MentoringBlock):
blocks_found.append(block.scope_ids.usage_id)
elif block.has_children:
for child_id in block.children:
find_mentoring_blocks(block.runtime.get_block(child_id))
find_mentoring_blocks(course)
total = len(blocks_found)
print(" ➔ Found {} mentoring blocks".format(total))
with store.bulk_operations(course.location.course_key):
count = 1
for block_id in blocks_found:
block = course.runtime.get_block(block_id)
print(" ➔ Upgrading block {} of {} - \"{}\"".format(count, total, block.url_name))
count += 1
upgrade_block(block)
print(" ➔ Complete.")
# -*- coding: utf-8 -*-
"""
Each class in this file represents a change made to the XML schema between v1 and v2.
"""
from lxml import etree
import warnings
class Change(object):
@staticmethod
def applies_to(node):
"""
Does this change affect the given XML node?
n.b. prior Changes will already be applied to the node.
"""
raise NotImplementedError
def __init__(self, node):
"""
Prepare to upgrade 'node' at some point in the future
"""
self.node = node
def apply(self):
raise NotImplementedError
class RemoveTitle(Change):
""" The old <title> element is now an attribute of <mentoring> """
@staticmethod
def applies_to(node):
return node.tag == "title" and node.getparent().tag == "mentoring"
def apply(self):
title = self.node.text.strip()
p = self.node.getparent()
old_display_name = p.get("display_name")
if old_display_name and old_display_name != title:
warnings.warn('Replacing display_name="{}" with <title> value "{}"'.format(p.attrib["display_name"], title))
p.attrib["display_name"] = title
p.remove(self.node)
class UnwrapHTML(Change):
""" <choice>,<tip>, <header>, and <message> now contain HTML without an explicit <html> wrapper. """
@staticmethod
def applies_to(node):
return node.tag == "html" and node.getparent().tag in ("choice", "tip", "message", "header")
def apply(self):
p = self.node.getparent()
if self.node.text:
p.text = (p.text if p.text else u"") + self.node.text
index = list(p).index(self.node)
for child in list(self.node):
index += 1
p.insert(index, child)
p.remove(self.node)
class TableColumnHeader(Change):
"""
Replace:
<mentoring-table>
<column>
<header>Answer 1</header>
<answer name="answer_1" />
</column>
</mentoring-table>
with
<mentoring-table>
<column header="Answer 1">
<answer-recap name="answer_1" />
</column>
</mentoring-table>
"""
@staticmethod
def applies_to(node):
return node.tag == "column" and node.getparent().tag == "mentoring-table"
def apply(self):
header_html = u""
to_remove = []
for child in list(self.node):
if child.tag == "header":
if child.text:
header_html += child.text
for grandchild in list(child):
header_html += etree.tostring(grandchild)
to_remove.append(child)
elif child.tag == "answer":
child.tag = "answer-recap"
if "read_only" in child.attrib:
del child.attrib["read_only"]
elif child.tag != "html":
warnings.warn("Invalid <column> element: Unexpected <{}>".format(child.tag))
return
for child in to_remove:
self.node.remove(child)
self.node.text = None
if header_html:
self.node.attrib["header"] = header_html
class QuizzToMCQ(Change):
""" <quizz> element was an alias for <mcq>. In v2 we only have <mcq> """
@staticmethod
def applies_to(node):
return node.tag == "quizz"
def apply(self):
self.node.tag = "mcq"
class MCQToRating(Change):
""" <mcq type="rating"> is now just <rating>, and we never use type="choices" on MCQ/MRQ """
@staticmethod
def applies_to(node):
return node.tag in ("mcq", "mrq") and "type" in node.attrib
def apply(self):
if self.node.tag == "mcq" and self.node.get("type") == "rating":
self.node.tag = "rating"
self.node.attrib.pop("type") # Type attribute is no longer used.
class ReadOnlyAnswerToRecap(Change):
""" <answer read_only="true"> is now <answer-recap/> """
@staticmethod
def applies_to(node):
return node.tag == "answer" and node.get("read_only") == "true"
def apply(self):
self.node.tag = "answer-recap"
self.node.attrib
self.node.attrib.pop("read_only")
for name in self.node.attrib:
if name != "name":
warnings.warn("Invalid attribute found on <answer>: {}".format(name))
class QuestionToField(Change):
"""
<answer/mcq/mrq/rating>
<question>What is the answer?</question>
</answer/mcq/mrq/rating>
has become
<answer/mcq/mrq question="What is the answer?"></answer>
"""
@staticmethod
def applies_to(node):
parent_tags = ("answer", "mcq", "mrq", "rating")
return node.tag == "question" and node.getparent().tag in parent_tags
def apply(self):
if list(self.node):
warnings.warn("Ignoring unexpected children of a <question> element. HTML may be lost.")
p = self.node.getparent()
p.attrib["question"] = self.node.text
p.remove(self.node)
class QuestionSubmitMessageToField(Change):
"""
<mcq/mrq>
<message type="on-submit">Thank you for answering!</message>
</mcq/mrq>
has become
<mcq/mrq message="Thank you for answering!"></answer>
"""
@staticmethod
def applies_to(node):
return node.tag == "message" and node.get("type") == "on-submit" and node.getparent().tag in ("mcq", "mrq")
def apply(self):
if list(self.node):
warnings.warn("Ignoring unexpected children of a <message> element. HTML may be lost.")
p = self.node.getparent()
p.attrib["message"] = self.node.text
p.remove(self.node)
class TipChanges(Change):
"""
Changes to <tip></tip> elements.
The main one being that the correctness of each choice is now stored on the MRQ/MCQ block, not on the <tip>s.
"""
@staticmethod
def applies_to(node):
return node.tag == "tip" and node.getparent().tag in ("mcq", "mrq", "rating")
def apply(self):
p = self.node.getparent()
def add_to_list(list_name, value):
if list_name in p.attrib:
p.attrib[list_name] += ",{}".format(value)
else:
p.attrib[list_name] = value
if len(self.node.attrib) > 1:
warnings.warn("Invalid <tip> element found.")
return
mode = self.node.attrib.keys()[0]
value = self.node.attrib[mode]
if p.tag == "mrq":
if mode == "display":
add_to_list("ignored_choices", value)
elif mode == "require":
add_to_list("required_choices", value)
elif mode != "reject":
warnings.warn("Invalid <tip> element: has {}={}".format(mode, value))
return
else:
# This is an MCQ or Rating question:
if mode == "display":
add_to_list("correct_choices", value)
elif mode != "reject":
warnings.warn("Invalid <tip> element: has {}={}".format(mode, value))
return
self.node.attrib["values"] = value
self.node.attrib.pop(mode)
class SharedHeaderToHTML(Change):
""" <shared-header> element no longer exists. Just use <html> """
@staticmethod
def applies_to(node):
return node.tag == "shared-header" and node.getparent().tag == "mentoring"
def apply(self):
self.node.tag = "html"
# An *ordered* list of all XML schema changes:
xml_changes = (
RemoveTitle,
UnwrapHTML,
TableColumnHeader,
QuizzToMCQ,
MCQToRating,
ReadOnlyAnswerToRecap,
QuestionToField,
QuestionSubmitMessageToField,
TipChanges,
SharedHeaderToHTML,
)
def convert_xml_v1_to_v2(node):
"""
Given an XML node, re-structure it as needed to convert it from v1 style to v2 style XML.
"""
# Apply each individual type of change one at a time:
for change in xml_changes:
# Walk the XML tree once and figure out all the changes we will need.
# This lets us avoid modifying the tree while walking it.
changes_needed = []
for element in node.iter():
if change.applies_to(element):
changes_needed.append(change(element))
for change in changes_needed:
change.apply()
......@@ -9,16 +9,10 @@ because the workbench SDK's settings file is not inside any python module.
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "workbench.settings")
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
......@@ -29,6 +23,6 @@ if __name__ == "__main__":
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["mentoring/tests/"]
paths = ["mentoring/tests/", "mentoring/v1/tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment