+
+
+
+
+
The main goal of this exercise is to start practicing the art of slow reading.
+
+
+
+
+ |87 No, those who are really responsible are Zeus and Fate [Moira] and the Fury [Erinys] who roams in the mist.
+ |88 They are the ones who
+ |100 He [= Zeus], making a formal declaration [eukhesthai], spoke up at a meeting of all the gods and said:
+ |101 “hear me, all gods and all goddesses,
+ |113 but he swore a great oath.
+ And right then and there
+
+
+
+
+
+
+
+
diff --git a/common/lib/xmodule/xmodule/js/spec/annotatable/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/annotatable/display_spec.coffee
new file mode 100644
index 0000000000..3adb028f97
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/annotatable/display_spec.coffee
@@ -0,0 +1,9 @@
+describe 'Annotatable', ->
+ beforeEach ->
+ loadFixtures 'annotatable.html'
+ describe 'constructor', ->
+ el = $('.xmodule_display.xmodule_AnnotatableModule')
+ beforeEach ->
+ @annotatable = new Annotatable(el)
+ it 'works', ->
+ expect(1).toBe(1)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
new file mode 100644
index 0000000000..2ad49ae6d7
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
@@ -0,0 +1,197 @@
+class @Annotatable
+ _debug: false
+
+ # selectors for the annotatable xmodule
+ toggleAnnotationsSelector: '.annotatable-toggle-annotations'
+ toggleInstructionsSelector: '.annotatable-toggle-instructions'
+ instructionsSelector: '.annotatable-instructions'
+ sectionSelector: '.annotatable-section'
+ spanSelector: '.annotatable-span'
+ replySelector: '.annotatable-reply'
+
+ # these selectors are for responding to events from the annotation capa problem type
+ problemXModuleSelector: '.xmodule_CapaModule'
+ problemSelector: 'section.problem'
+ problemInputSelector: 'section.problem .annotation-input'
+ problemReturnSelector: 'section.problem .annotation-return'
+
+ constructor: (el) ->
+ console.log 'loaded Annotatable' if @_debug
+ @el = el
+ @$el = $(el)
+ @init()
+
+ $: (selector) ->
+ $(selector, @el)
+
+ init: () ->
+ @initEvents()
+ @initTips()
+
+ initEvents: () ->
+ # Initialize toggle handlers for the instructions and annotations sections
+ [@annotationsHidden, @instructionsHidden] = [false, false]
+ @$(@toggleAnnotationsSelector).bind 'click', @onClickToggleAnnotations
+ @$(@toggleInstructionsSelector).bind 'click', @onClickToggleInstructions
+
+ # Initialize handler for 'reply to annotation' events that scroll to
+ # the associated problem. The reply buttons are part of the tooltip
+ # content. It's important that the tooltips be configured to render
+ # as descendants of the annotation module and *not* the document.body.
+ @$el.delegate @replySelector, 'click', @onClickReply
+
+ # Initialize handler for 'return to annotation' events triggered from problems.
+ # 1) There are annotationinput capa problems rendered on the page
+ # 2) Each one has an embedded return link (see annotation capa problem template).
+ # Since the capa problem injects HTML content via AJAX, the best we can do is
+ # is let the click events bubble up to the body and handle them there.
+ $('body').delegate @problemReturnSelector, 'click', @onClickReturn
+
+ initTips: () ->
+ # tooltips are used to display annotations for highlighted text spans
+ @$(@spanSelector).each (index, el) =>
+ $(el).qtip(@getSpanTipOptions el)
+
+ getSpanTipOptions: (el) ->
+ content:
+ title:
+ text: @makeTipTitle(el)
+ text: @makeTipContent(el)
+ position:
+ my: 'bottom center' # of tooltip
+ at: 'top center' # of target
+ target: $(el) # where the tooltip was triggered (i.e. the annotation span)
+ container: @$el
+ adjust:
+ y: -5
+ show:
+ event: 'click mouseenter'
+ solo: true
+ hide:
+ event: 'click mouseleave'
+ delay: 500,
+ fixed: true # don't hide the tooltip if it is moused over
+ style:
+ classes: 'ui-tooltip-annotatable'
+ events:
+ show: @onShowTip
+
+ onClickToggleAnnotations: (e) => @toggleAnnotations()
+
+ onClickToggleInstructions: (e) => @toggleInstructions()
+
+ onClickReply: (e) => @replyTo(e.currentTarget)
+
+ onClickReturn: (e) => @returnFrom(e.currentTarget)
+
+ onShowTip: (event, api) =>
+ event.preventDefault() if @annotationsHidden
+
+ getSpanForProblemReturn: (el) ->
+ problem_id = $(@problemReturnSelector).index(el)
+ @$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
+
+ getProblem: (el) ->
+ problem_id = @getProblemId(el)
+ $(@problemSelector).has(@problemInputSelector).eq(problem_id)
+
+ getProblemId: (el) ->
+ $(el).data('problem-id')
+
+ toggleAnnotations: () ->
+ hide = (@annotationsHidden = not @annotationsHidden)
+ @toggleAnnotationButtonText hide
+ @toggleSpans hide
+ @toggleTips hide
+
+ toggleTips: (hide) ->
+ visible = @findVisibleTips()
+ @hideTips visible
+
+ toggleAnnotationButtonText: (hide) ->
+ buttonText = (if hide then 'Show' else 'Hide')+' Annotations'
+ @$(@toggleAnnotationsSelector).text(buttonText)
+
+ toggleInstructions: () ->
+ hide = (@instructionsHidden = not @instructionsHidden)
+ @toggleInstructionsButton hide
+ @toggleInstructionsText hide
+
+ toggleInstructionsButton: (hide) ->
+ txt = (if hide then 'Expand' else 'Collapse')+' Instructions'
+ cls = (if hide then ['expanded', 'collapsed'] else ['collapsed','expanded'])
+ @$(@toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1])
+
+ toggleInstructionsText: (hide) ->
+ slideMethod = (if hide then 'slideUp' else 'slideDown')
+ @$(@instructionsSelector)[slideMethod]()
+
+ toggleSpans: (hide) ->
+ @$(@spanSelector).toggleClass 'hide', hide, 250
+
+ replyTo: (buttonEl) ->
+ offset = -20
+ el = @getProblem buttonEl
+ if el.length > 0
+ @scrollTo(el, @afterScrollToProblem, offset)
+ else
+ console.log('problem not found. event: ', e) if @_debug
+
+ returnFrom: (buttonEl) ->
+ offset = -200
+ el = @getSpanForProblemReturn buttonEl
+ if el.length > 0
+ @scrollTo(el, @afterScrollToSpan, offset)
+ else
+ console.log('span not found. event:', e) if @_debug
+
+ scrollTo: (el, after, offset = -20) ->
+ $('html,body').scrollTo(el, {
+ duration: 500
+ onAfter: @_once => after?.call this, el
+ offset: offset
+ }) if $(el).length > 0
+
+ afterScrollToProblem: (problem_el) ->
+ problem_el.effect 'highlight', {}, 500
+
+ afterScrollToSpan: (span_el) ->
+ span_el.addClass 'selected', 400, 'swing', ->
+ span_el.removeClass 'selected', 400, 'swing'
+
+ makeTipContent: (el) ->
+ (api) =>
+ text = $(el).data('comment-body')
+ comment = @createComment(text)
+ problem_id = @getProblemId(el)
+ reply = @createReplyLink(problem_id)
+ $(comment).add(reply)
+
+ makeTipTitle: (el) ->
+ (api) =>
+ title = $(el).data('comment-title')
+ (if title then title else 'Commentary')
+
+ createComment: (text) ->
+ $("")
+
+ createReplyLink: (problem_id) ->
+ $("
Reply to Annotation")
+
+ findVisibleTips: () ->
+ visible = []
+ @$(@spanSelector).each (index, el) ->
+ api = $(el).qtip('api')
+ tip = $(api?.elements.tooltip)
+ if tip.is(':visible')
+ visible.push el
+ visible
+
+ hideTips: (elements) ->
+ $(elements).qtip('hide')
+
+ _once: (fn) ->
+ done = false
+ return =>
+ fn.call this unless done
+ done = true
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 41c9b50891..158c2b98d0 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -76,6 +76,24 @@ class @Problem
# TODO: Some logic to dynamically adjust polling rate based on queuelen
window.queuePollerID = window.setTimeout(@poll, 1000)
+
+ # Use this if you want to make an ajax call on the input type object
+ # static method so you don't have to instantiate a Problem in order to use it
+ # Input:
+ # url: the AJAX url of the problem
+ # input_id: the input_id of the input you would like to make the call on
+ # NOTE: the id is the ${id} part of "input_${id}" during rendering
+ # If this function is passed the entire prefixed id, the backend may have trouble
+ # finding the correct input
+ # dispatch: string that indicates how this data should be handled by the inputtype
+ # callback: the function that will be called once the AJAX call has been completed.
+ # It will be passed a response object
+ @inputAjax: (url, input_id, dispatch, data, callback) ->
+ data['dispatch'] = dispatch
+ data['input_id'] = input_id
+ $.postWithPrefix "#{url}/input_ajax", data, callback
+
+
render: (content) ->
if content
@el.html(content)
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index db8171e945..bca7dbfed9 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -75,6 +75,11 @@ class SequenceModule(XModule):
raise NotFoundError('Unexpected dispatch type')
def render(self):
+ # If we're rendering this sequence, but no position is set yet,
+ # default the position to the first element
+ if self.position is None:
+ self.position = 1
+
if self.rendered:
return
## Returns a set of all types of all sub-children
diff --git a/common/lib/xmodule/xmodule/templates/annotatable/default.yaml b/common/lib/xmodule/xmodule/templates/annotatable/default.yaml
new file mode 100644
index 0000000000..31dd489fb4
--- /dev/null
+++ b/common/lib/xmodule/xmodule/templates/annotatable/default.yaml
@@ -0,0 +1,20 @@
+---
+metadata:
+ display_name: 'Annotation'
+data: |
+
+
+ Enter your (optional) instructions for the exercise in HTML format.
+ Annotations are specified by an <annotation> tag which may may have the following attributes:
+
+ title (optional). Title of the annotation. Defaults to Commentary if omitted.
+ body (required). Text of the annotation.
+ problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".
+ highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.
+
+
+ Add your HTML with annotation spans here.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sodales laoreet est, egestas gravida felis egestas nec. Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.
+ Nulla facilisi. Pellentesque id vestibulum libero. Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.
+
+children: []
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 8db3d72aec..1a10654f6c 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -28,22 +28,36 @@ open_ended_grading_interface = {
'grading_controller' : 'grading_controller'
}
-test_system = ModuleSystem(
- ajax_url='courses/course_id/modx/a_location',
- track_function=Mock(),
- get_module=Mock(),
- # "render" to just the context...
- render_template=lambda template, context: str(context),
- replace_urls=Mock(),
- user=Mock(is_staff=False),
- filestore=Mock(),
- debug=True,
- xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
- node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
- xblock_model_data=lambda descriptor: descriptor._model_data,
- anonymous_student_id='student',
- open_ended_grading_interface=open_ended_grading_interface,
-)
+
+def test_system():
+ """
+ Construct a test ModuleSystem instance.
+
+ By default, the render_template() method simply returns
+ the context it is passed as a string.
+ You can override this behavior by monkey patching:
+
+ system = test_system()
+ system.render_template = my_render_func
+
+ where my_render_func is a function of the form
+ my_render_func(template, context)
+ """
+ return ModuleSystem(
+ ajax_url='courses/course_id/modx/a_location',
+ track_function=Mock(),
+ get_module=Mock(),
+ render_template=lambda template, context: str(context),
+ replace_urls=lambda html: str(html),
+ user=Mock(is_staff=False),
+ filestore=Mock(),
+ debug=True,
+ xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
+ node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
+ xblock_model_data=lambda descriptor: descriptor._model_data,
+ anonymous_student_id='student',
+ open_ended_grading_interface= open_ended_grading_interface
+ )
class ModelsTest(unittest.TestCase):
diff --git a/common/lib/xmodule/xmodule/tests/test_annotatable_module.py b/common/lib/xmodule/xmodule/tests/test_annotatable_module.py
new file mode 100644
index 0000000000..30f9c9ff92
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_annotatable_module.py
@@ -0,0 +1,129 @@
+"""Module annotatable tests"""
+
+import unittest
+
+from lxml import etree
+from mock import Mock
+
+from xmodule.annotatable_module import AnnotatableModule
+from xmodule.modulestore import Location
+
+from . import test_system
+
+class AnnotatableModuleTestCase(unittest.TestCase):
+ location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
+ sample_xml = '''
+
+ Read the text.
+
+ Sing,
+ O goddess,
+ the anger of Achilles son of Peleus,
+ that brought countless ills upon the Achaeans. Many a brave soul did it send
+ hurrying down to Hades, and many a hero did it yield a prey to dogs and
+
vultures, for so were the counsels
+ of Jove fulfilled from the day on which the son of Atreus, king of men, and great
+ Achilles, first fell out with one another.
+
+ The Iliad of Homer by Samuel Butler
+
+ '''
+ definition = { 'data': sample_xml }
+ descriptor = Mock()
+ instance_state = None
+ shared_state = None
+
+ def setUp(self):
+ self.annotatable = AnnotatableModule(test_system(), self.location, self.definition, self.descriptor, self.instance_state, self.shared_state)
+
+ def test_annotation_data_attr(self):
+ el = etree.fromstring('
test')
+
+ expected_attr = {
+ 'data-comment-body': {'value': 'foo', '_delete': 'body' },
+ 'data-comment-title': {'value': 'bar', '_delete': 'title'},
+ 'data-problem-id': {'value': '0', '_delete': 'problem'}
+ }
+
+ actual_attr = self.annotatable._get_annotation_data_attr(0, el)
+
+ self.assertTrue(type(actual_attr) is dict)
+ self.assertDictEqual(expected_attr, actual_attr)
+
+ def test_annotation_class_attr_default(self):
+ xml = '
test'
+ el = etree.fromstring(xml)
+
+ expected_attr = { 'class': { 'value': 'annotatable-span highlight' } }
+ actual_attr = self.annotatable._get_annotation_class_attr(0, el)
+
+ self.assertTrue(type(actual_attr) is dict)
+ self.assertDictEqual(expected_attr, actual_attr)
+
+ def test_annotation_class_attr_with_valid_highlight(self):
+ xml = '
test'
+
+ for color in self.annotatable.highlight_colors:
+ el = etree.fromstring(xml.format(highlight=color))
+ value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
+
+ expected_attr = { 'class': {
+ 'value': value,
+ '_delete': 'highlight' }
+ }
+ actual_attr = self.annotatable._get_annotation_class_attr(0, el)
+
+ self.assertTrue(type(actual_attr) is dict)
+ self.assertDictEqual(expected_attr, actual_attr)
+
+ def test_annotation_class_attr_with_invalid_highlight(self):
+ xml = '
test'
+
+ for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
+ el = etree.fromstring(xml.format(highlight=invalid_color))
+ expected_attr = { 'class': {
+ 'value': 'annotatable-span highlight',
+ '_delete': 'highlight' }
+ }
+ actual_attr = self.annotatable._get_annotation_class_attr(0, el)
+
+ self.assertTrue(type(actual_attr) is dict)
+ self.assertDictEqual(expected_attr, actual_attr)
+
+ def test_render_annotation(self):
+ expected_html = '
z'
+ expected_el = etree.fromstring(expected_html)
+
+ actual_el = etree.fromstring('
z')
+ self.annotatable._render_annotation(0, actual_el)
+
+ self.assertEqual(expected_el.tag, actual_el.tag)
+ self.assertEqual(expected_el.text, actual_el.text)
+ self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
+
+ def test_render_content(self):
+ content = self.annotatable._render_content()
+ el = etree.fromstring(content)
+
+ self.assertEqual('div', el.tag, 'root tag is a div')
+
+ expected_num_annotations = 5
+ actual_num_annotations = el.xpath('count(//span[contains(@class,"annotatable-span")])')
+ self.assertEqual(expected_num_annotations, actual_num_annotations, 'check number of annotations')
+
+ def test_get_html(self):
+ context = self.annotatable.get_html()
+ for key in ['display_name', 'element_id', 'content_html', 'instructions_html']:
+ self.assertIn(key, context)
+
+ def test_extract_instructions(self):
+ xmltree = etree.fromstring(self.sample_xml)
+
+ expected_xml = u"
Read the text.
"
+ actual_xml = self.annotatable._extract_instructions(xmltree)
+ self.assertIsNotNone(actual_xml)
+ self.assertEqual(expected_xml.strip(), actual_xml.strip())
+
+ xmltree = etree.fromstring('
foo')
+ actual = self.annotatable._extract_instructions(xmltree)
+ self.assertIsNone(actual)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index dbcee08c0c..ca8f267c39 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,13 +1,18 @@
import datetime
import json
-from mock import Mock
+from mock import Mock, MagicMock, patch
from pprint import pprint
import unittest
+import random
+import xmodule
+import capa
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
+from django.http import QueryDict
+
from . import test_system
@@ -33,6 +38,18 @@ class CapaFactory(object):
CapaFactory.num += 1
return CapaFactory.num
+ @staticmethod
+ def input_key():
+ """ Return the input key to use when passing GET parameters """
+ return ("input_" + CapaFactory.answer_key())
+
+ @staticmethod
+ def answer_key():
+ """ Return the key stored in the capa problem answer dict """
+ return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
+ 'SampleProblem%d' % CapaFactory.num]) +
+ "_2_1")
+
@staticmethod
def create(graceperiod=None,
due=None,
@@ -59,7 +76,6 @@ class CapaFactory(object):
module.
attempts: also added to instance state. Will be converted to an int.
- correct: if True, the problem will be initialized to be answered correctly.
"""
definition = {'data': CapaFactory.sample_problem_xml, }
location = Location(["i4x", "edX", "capa_test", "problem",
@@ -87,13 +103,14 @@ class CapaFactory(object):
# since everything else is a string.
model_data['attempts'] = int(attempts)
- if correct:
- # TODO: make this actually set an answer of 3.14, and mark it correct
- #instance_state_dict['student_answers'] = {}
- #instance_state_dict['correct_map'] = {}
- pass
+ if len(instance_state_dict) > 0:
+ instance_state = json.dumps(instance_state_dict)
+ else:
+ instance_state = None
- module = CapaModule(test_system, location, descriptor, model_data)
+ system = test_system()
+ system.render_template = Mock(return_value="
Test Template HTML
")
+ module = CapaModule(system, location, descriptor, model_data)
if correct:
# TODO: probably better to actually set the internal state properly, but...
@@ -127,6 +144,8 @@ class CapaModuleTest(unittest.TestCase):
"Factory should be creating unique names for each problem")
+
+
def test_correct(self):
"""
Check that the factory creates correct and incorrect problems properly.
@@ -170,6 +189,7 @@ class CapaModuleTest(unittest.TestCase):
max_attempts="1",
attempts="0",
due=self.yesterday_str)
+
self.assertTrue(after_due_date.answer_available())
@@ -274,3 +294,602 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertTrue(still_in_grace.answer_available())
+
+
+ def test_closed(self):
+
+ # Attempts < Max attempts --> NOT closed
+ module = CapaFactory.create(max_attempts="1", attempts="0")
+ self.assertFalse(module.closed())
+
+ # Attempts < Max attempts --> NOT closed
+ module = CapaFactory.create(max_attempts="2", attempts="1")
+ self.assertFalse(module.closed())
+
+ # Attempts = Max attempts --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="1")
+ self.assertTrue(module.closed())
+
+ # Attempts > Max attempts --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="2")
+ self.assertTrue(module.closed())
+
+ # Max attempts = 0 --> closed
+ module = CapaFactory.create(max_attempts="0", attempts="2")
+ self.assertTrue(module.closed())
+
+ # Past due --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="0",
+ due=self.yesterday_str)
+ self.assertTrue(module.closed())
+
+
+ def test_parse_get_params(self):
+
+ # We have to set up Django settings in order to use QueryDict
+ from django.conf import settings
+ settings.configure()
+
+ # Valid GET param dict
+ valid_get_dict = self._querydict_from_dict({'input_1': 'test',
+ 'input_1_2': 'test',
+ 'input_1_2_3': 'test',
+ 'input_[]_3': 'test',
+ 'input_4': None,
+ 'input_5': [],
+ 'input_6': 5})
+
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+
+ # Expect that we get a dict with "input" stripped from key names
+ # and that we get the same values back
+ for key in result.keys():
+ original_key = "input_" + key
+ self.assertTrue(original_key in valid_get_dict,
+ "Output dict should have key %s" % original_key)
+ self.assertEqual(valid_get_dict[original_key], result[key])
+
+
+ # Valid GET param dict with list keys
+ valid_get_dict = self._querydict_from_dict({'input_2[]': ['test1', 'test2']})
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+ self.assertTrue('2' in result)
+ self.assertEqual(['test1','test2'], result['2'])
+
+ # If we use [] at the end of a key name, we should always
+ # get a list, even if there's just one value
+ valid_get_dict = self._querydict_from_dict({'input_1[]': 'test'})
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+ self.assertEqual(result['1'], ['test'])
+
+ # If we have no underscores in the name, then the key is invalid
+ invalid_get_dict = self._querydict_from_dict({'input': 'test'})
+ with self.assertRaises(ValueError):
+ result = CapaModule.make_dict_of_responses(invalid_get_dict)
+
+
+ # Two equivalent names (one list, one non-list)
+ # One of the values would overwrite the other, so detect this
+ # and raise an exception
+ invalid_get_dict = self._querydict_from_dict({'input_1[]': 'test 1',
+ 'input_1': 'test 2' })
+ with self.assertRaises(ValueError):
+ result = CapaModule.make_dict_of_responses(invalid_get_dict)
+
+ def _querydict_from_dict(self, param_dict):
+ """ Create a Django QueryDict from a Python dictionary """
+
+ # QueryDict objects are immutable by default, so we make
+ # a copy that we can update.
+ querydict = QueryDict('')
+ copyDict = querydict.copy()
+
+ for (key, val) in param_dict.items():
+
+ # QueryDicts handle lists differently from ordinary values,
+ # so we have to specifically tell the QueryDict that
+ # this is a list
+ if type(val) is list:
+ copyDict.setlist(key, val)
+ else:
+ copyDict[key] = val
+
+ return copyDict
+
+
+ def test_check_problem_correct(self):
+
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate that all answers are marked correct, no matter
+ # what the input is, by patching CorrectMap.is_correct()
+ # Also simulate rendering the HTML
+ with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
+ patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
+ mock_is_correct.return_value = True
+ mock_html.return_value = "Test HTML"
+
+ # Check the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect that the problem is marked correct
+ self.assertEqual(result['success'], 'correct')
+
+ # Expect that we get the (mocked) HTML
+ self.assertEqual(result['contents'], 'Test HTML')
+
+ # Expect that the number of attempts is incremented by 1
+ self.assertEqual(module.attempts, 2)
+
+
+ def test_check_problem_incorrect(self):
+
+ module = CapaFactory.create(attempts=0)
+
+ # Simulate marking the input incorrect
+ with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
+ mock_is_correct.return_value = False
+
+ # Check the problem
+ get_request_dict = { CapaFactory.input_key(): '0' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect that the problem is marked correct
+ self.assertEqual(result['success'], 'incorrect')
+
+ # Expect that the number of attempts is incremented by 1
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_closed(self):
+ module = CapaFactory.create(attempts=3)
+
+ # Problem closed -- cannot submit
+ # Simulate that CapaModule.closed() always returns True
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+ with self.assertRaises(xmodule.exceptions.NotFoundError):
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ module.check_problem(get_request_dict)
+
+ # Expect that number of attempts NOT incremented
+ self.assertEqual(module.attempts, 3)
+
+
+ def test_check_problem_resubmitted_with_randomize(self):
+ # Randomize turned on
+ module = CapaFactory.create(rerandomize='always', attempts=0)
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Expect that we cannot submit
+ with self.assertRaises(xmodule.exceptions.NotFoundError):
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ module.check_problem(get_request_dict)
+
+ # Expect that number of attempts NOT incremented
+ self.assertEqual(module.attempts, 0)
+
+
+ def test_check_problem_resubmitted_no_randomize(self):
+ # Randomize turned off
+ module = CapaFactory.create(rerandomize='never', attempts=0)
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Expect that we can submit successfully
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ self.assertEqual(result['success'], 'correct')
+
+ # Expect that number of attempts IS incremented
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_queued(self):
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate that the problem is queued
+ with patch('capa.capa_problem.LoncapaProblem.is_queued') \
+ as mock_is_queued,\
+ patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \
+ as mock_get_queuetime:
+
+ mock_is_queued.return_value = True
+ mock_get_queuetime.return_value = datetime.datetime.now()
+
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect an AJAX alert message in 'success'
+ self.assertTrue('You must wait' in result['success'])
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_student_input_error(self):
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate a student input exception
+ with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
+ mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
+
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect an AJAX alert message in 'success'
+ self.assertTrue('test error' in result['success'])
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_reset_problem(self):
+ module = CapaFactory.create()
+
+ # Mock the module's capa problem
+ # to simulate that the problem is done
+ mock_problem = MagicMock(capa.capa_problem.LoncapaProblem)
+ mock_problem.done = True
+ module.lcp = mock_problem
+
+ # Stub out HTML rendering
+ with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
+ mock_html.return_value = "
Test HTML
"
+
+ # Reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the request was successful
+ self.assertTrue('success' in result and result['success'])
+
+ # Expect that the problem HTML is retrieved
+ self.assertTrue('html' in result)
+ self.assertEqual(result['html'], "
Test HTML
")
+
+ # Expect that the problem was reset
+ mock_problem.do_reset.assert_called_once_with()
+
+
+ def test_reset_problem_closed(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is closed
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+
+ # Try to reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the problem was NOT reset
+ self.assertTrue('success' in result and not result['success'])
+
+
+ def test_reset_problem_not_done(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is NOT done
+ module.lcp.done = False
+
+ # Try to reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the problem was NOT reset
+ self.assertTrue('success' in result and not result['success'])
+
+
+ def test_save_problem(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is not done (not attempted or reset)
+ module.lcp.done = False
+
+ # Save the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that answers are saved to the problem
+ expected_answers = { CapaFactory.answer_key(): '3.14' }
+ self.assertEqual(module.lcp.student_answers, expected_answers)
+
+ # Expect that the result is success
+ self.assertTrue('success' in result and result['success'])
+
+
+ def test_save_problem_closed(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is NOT done (not attempted or reset)
+ module.lcp.done = False
+
+ # Simulate that the problem is closed
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+
+ # Try to save the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that the result is failure
+ self.assertTrue('success' in result and not result['success'])
+
+
+ def test_save_problem_submitted_with_randomize(self):
+ module = CapaFactory.create(rerandomize='always')
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Try to save
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that we cannot save
+ self.assertTrue('success' in result and not result['success'])
+
+
+ def test_save_problem_submitted_no_randomize(self):
+ module = CapaFactory.create(rerandomize='never')
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Try to save
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that we succeed
+ self.assertTrue('success' in result and result['success'])
+
+ def test_check_button_name(self):
+
+ # If last attempt, button name changes to "Final Check"
+ # Just in case, we also check what happens if we have
+ # more attempts than allowed.
+ attempts = random.randint(1, 10)
+ module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ module = CapaFactory.create(attempts=attempts + 1, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ # Otherwise, button name is "Check"
+ module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ # If no limit on attempts, then always show "Check"
+ module = CapaFactory.create(attempts=attempts-3)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ module = CapaFactory.create(attempts=0)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ def test_should_show_check_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show check button
+ module = CapaFactory.create(due=self.yesterday_str)
+ self.assertFalse(module.should_show_check_button())
+
+ # If user is out of attempts, do NOT show the check button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ self.assertFalse(module.should_show_check_button())
+
+ # If survey question (max_attempts = 0), do NOT show the check button
+ module = CapaFactory.create(max_attempts=0)
+ self.assertFalse(module.should_show_check_button())
+
+ # If user submitted a problem but hasn't reset,
+ # do NOT show the check button
+ # Note: we can only reset when rerandomize="always"
+ module = CapaFactory.create(rerandomize="always")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_check_button())
+
+ # Otherwise, DO show the check button
+ module = CapaFactory.create()
+ self.assertTrue(module.should_show_check_button())
+
+ # If the user has submitted the problem
+ # and we do NOT have a reset button, then we can show the check button
+ # Setting rerandomize to "never" ensures that the reset button
+ # is not shown
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_check_button())
+
+
+ def test_should_show_reset_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show the reset button
+ module = CapaFactory.create(due=self.yesterday_str)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If the user is out of attempts, do NOT show the reset button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If we're NOT randomizing, then do NOT show the reset button
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If the user hasn't submitted an answer yet,
+ # then do NOT show the reset button
+ module = CapaFactory.create()
+ module.lcp.done = False
+ self.assertFalse(module.should_show_reset_button())
+
+ # Otherwise, DO show the reset button
+ module = CapaFactory.create()
+ module.lcp.done = True
+ self.assertTrue(module.should_show_reset_button())
+
+ # If survey question for capa (max_attempts = 0),
+ # DO show the reset button
+ module = CapaFactory.create(max_attempts=0)
+ module.lcp.done = True
+ self.assertTrue(module.should_show_reset_button())
+
+
+ def test_should_show_save_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show the save button
+ module = CapaFactory.create(due=self.yesterday_str)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If the user is out of attempts, do NOT show the save button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If user submitted a problem but hasn't reset, do NOT show the save button
+ module = CapaFactory.create(rerandomize="always")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # Otherwise, DO show the save button
+ module = CapaFactory.create()
+ module.lcp.done = False
+ self.assertTrue(module.should_show_save_button())
+
+ # If we're not randomizing, then we can re-save
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_save_button())
+
+ # If survey question for capa (max_attempts = 0),
+ # DO show the save button
+ module = CapaFactory.create(max_attempts=0)
+ module.lcp.done = False
+ self.assertTrue(module.should_show_save_button())
+
+ def test_should_show_save_button_force_save_button(self):
+ # If we're after the deadline, do NOT show the save button
+ # even though we're forcing a save
+ module = CapaFactory.create(due=self.yesterday_str,
+ force_save_button="true")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If the user is out of attempts, do NOT show the save button
+ attempts = random.randint(1,10)
+ module = CapaFactory.create(attempts=attempts,
+ max_attempts=attempts,
+ force_save_button="true")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # Otherwise, if we force the save button,
+ # then show it even if we would ordinarily
+ # require a reset first
+ module = CapaFactory.create(force_save_button="true",
+ rerandomize="always")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_save_button())
+
+ def test_no_max_attempts(self):
+ module = CapaFactory.create(max_attempts='')
+ html = module.get_problem_html()
+ # assert that we got here without exploding
+
+
+ def test_get_problem_html(self):
+ module = CapaFactory.create()
+
+ # We've tested the show/hide button logic in other tests,
+ # so here we hard-wire the values
+ show_check_button = bool(random.randint(0,1) % 2)
+ show_reset_button = bool(random.randint(0,1) % 2)
+ show_save_button = bool(random.randint(0,1) % 2)
+
+ module.should_show_check_button = Mock(return_value=show_check_button)
+ module.should_show_reset_button = Mock(return_value=show_reset_button)
+ module.should_show_save_button = Mock(return_value=show_save_button)
+
+ # Mock the system rendering function
+ module.system.render_template = Mock(return_value="
Test Template HTML
")
+
+ # Patch the capa problem's HTML rendering
+ with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
+ mock_html.return_value = "
Test Problem HTML
"
+
+ # Render the problem HTML
+ html = module.get_problem_html(encapsulate=False)
+
+ # Also render the problem encapsulated in a
+ html_encapsulated = module.get_problem_html(encapsulate=True)
+
+ # Expect that we get the rendered template back
+ self.assertEqual(html, "
Test Template HTML
")
+
+ # Check the rendering context
+ render_args,_ = module.system.render_template.call_args
+ self.assertEqual(len(render_args), 2)
+
+ template_name = render_args[0]
+ self.assertEqual(template_name, "problem.html")
+
+ context = render_args[1]
+ self.assertEqual(context['problem']['html'], "
Test Problem HTML
")
+ self.assertEqual(bool(context['check_button']), show_check_button)
+ self.assertEqual(bool(context['reset_button']), show_reset_button)
+ self.assertEqual(bool(context['save_button']), show_save_button)
+
+ # Assert that the encapsulated html contains the original html
+ self.assertTrue(html in html_encapsulated)
+
+
+ def test_get_problem_html_error(self):
+ """
+ In production, when an error occurs with the problem HTML
+ rendering, a "dummy" problem is created with an error
+ message to display to the user.
+ """
+ module = CapaFactory.create()
+
+ # Save the original problem so we can compare it later
+ original_problem = module.lcp
+
+ # Simulate throwing an exception when the capa problem
+ # is asked to render itself as HTML
+ module.lcp.get_html = Mock(side_effect=Exception("Test"))
+
+ # Stub out the test_system rendering function
+ module.system.render_template = Mock(return_value="
Test Template HTML
")
+
+ # Turn off DEBUG
+ module.system.DEBUG = False
+
+ # Try to render the module with DEBUG turned off
+ html = module.get_problem_html()
+
+ # Check the rendering context
+ render_args,_ = module.system.render_template.call_args
+ context = render_args[1]
+ self.assertTrue("error" in context['problem']['html'])
+
+ # Expect that the module has created a new dummy problem with the error
+ self.assertNotEqual(original_problem, module.lcp)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index c6e95df6a7..d3b869a802 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -54,8 +54,9 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- self.openendedchild = OpenEndedChild(test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.test_system = test_system()
+ self.openendedchild = OpenEndedChild(self.test_system, self.location,
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_latest_post_assessment_empty(self):
- answer = self.openendedchild.latest_post_assessment(test_system)
+ answer = self.openendedchild.latest_post_assessment(self.test_system)
self.assertEqual(answer, "")
@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_reset(self):
- self.openendedchild.reset(test_system)
+ self.openendedchild.reset(self.test_system)
state = json.loads(self.openendedchild.get_instance_state())
self.assertEqual(state['child_state'], OpenEndedChild.INITIAL)
@@ -182,12 +183,14 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- test_system.location = self.location
+ self.test_system = test_system()
+
+ self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
- test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
- self.openendedmodule = OpenEndedModule(test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
+ self.openendedmodule = OpenEndedModule(self.test_system, self.location,
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'grader_id': '1',
'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
- student_info = {'anonymous_student_id': test_system.anonymous_student_id,
+ student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = {
'feedback': get['feedback'],
@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_info': json.dumps(student_info)
}
- result = self.openendedmodule.message_post(get, test_system)
+ result = self.openendedmodule.message_post(get, self.test_system)
self.assertTrue(result['success'])
# make sure it's actually sending something we want to the queue
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self):
submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
- student_info = {'anonymous_student_id': test_system.anonymous_student_id,
+ student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = self.openendedmodule.payload.copy()
contents.update({
@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_response': submission,
'max_score': self.max_score
})
- result = self.openendedmodule.send_to_grader(submission, test_system)
+ result = self.openendedmodule.send_to_grader(submission, self.test_system)
self.assertTrue(result)
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': score_msg}
- self.openendedmodule.update_score(get, test_system)
+ self.openendedmodule.update_score(get, self.test_system)
def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry")
@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': json.dumps(score_msg)}
- self.openendedmodule.update_score(get, test_system)
+ self.openendedmodule.update_score(get, self.test_system)
def test_latest_post_assessment(self):
self.update_score_single()
- assessment = self.openendedmodule.latest_post_assessment(test_system)
+ assessment = self.openendedmodule.latest_post_assessment(self.test_system)
self.assertFalse(assessment == '')
# check for errors
self.assertFalse('errors' in assessment)
@@ -336,7 +339,16 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata, instance_state={})
+ self.test_system = test_system()
+ # TODO: this constructor call is definitely wrong, but neither branch
+ # of the merge matches the module constructor. Someone (Vik?) should fix this.
+ self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state={})
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("
Tag")
diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py
index 1d63577073..1b2da0b74a 100644
--- a/common/lib/xmodule/xmodule/tests/test_conditional.py
+++ b/common/lib/xmodule/xmodule/tests/test_conditional.py
@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
'''Get a dummy system'''
return DummySystem(load_error_modules)
+ def setUp(self):
+ self.test_system = test_system()
+
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
@@ -80,7 +83,7 @@ class ConditionalModuleTest(unittest.TestCase):
location = descriptor
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location
- return descriptor.xmodule(test_system)
+ return descriptor.xmodule(self.test_system)
# edx - HarvardX
# cond_test - ER22x
@@ -88,8 +91,8 @@ class ConditionalModuleTest(unittest.TestCase):
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
- test_system.replace_urls = replace_urls
- test_system.get_module = inner_get_module
+ self.test_system.replace_urls = replace_urls
+ self.test_system.get_module = inner_get_module
module = inner_get_module(location)
print "module: ", module
diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
index a2bd992317..43f3f3bc7a 100644
--- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py
+++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
@@ -51,13 +51,13 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks' : False,
}
- self.module = SelfAssessmentModule(test_system, self.location,
+ self.module = SelfAssessmentModule(test_system(), self.location,
self.definition,
self.descriptor,
static_data)
def test_get_html(self):
- html = self.module.get_html(test_system)
+ html = self.module.get_html(self.module.system)
self.assertTrue("This is sample prompt text" in html)
def test_self_assessment_flow(self):
@@ -80,10 +80,11 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.get_score()['score'], 0)
- self.module.save_answer({'student_answer': "I am an answer"}, test_system)
+ self.module.save_answer({'student_answer': "I am an answer"},
+ self.module.system)
self.assertEqual(self.module.child_state, self.module.ASSESSING)
- self.module.save_assessment(mock_query_dict, test_system)
+ self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.child_state, self.module.DONE)
@@ -92,7 +93,8 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.child_state, self.module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
- self.module.save_answer({'student_answer': 'answer 4'}, test_system)
+ self.module.save_answer({'student_answer': 'answer 4'},
+ self.module.system)
responses['assessment'] = '1'
- self.module.save_assessment(mock_query_dict, test_system)
+ self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.child_state, self.module.DONE)
diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py
index b92d7bdfb0..d327342d34 100644
--- a/common/lib/xmodule/xmodule/timelimit_module.py
+++ b/common/lib/xmodule/xmodule/timelimit_module.py
@@ -135,7 +135,7 @@ class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
continue
- return {'children': children}
+ return {}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('timelimit')
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 5062513beb..3713bfedb6 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -389,7 +389,11 @@ class XmlDescriptor(XModuleDescriptor):
if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
val = val_for_xml(attr)
#logging.debug('location.category = {0}, attr = {1}'.format(self.location.category, attr))
- xml_object.set(attr, val)
+ try:
+ xml_object.set(attr, val)
+ except Exception, e:
+ logging.exception('Failed to serialize metadata attribute {0} with value {1}. This could mean data loss!!! Exception: {2}'.format(attr, val, e))
+ pass
for key, value in self.xml_attributes.items():
if key not in self.metadata_to_strip:
diff --git a/common/static/coffee/src/discussion/discussion_module_view.coffee b/common/static/coffee/src/discussion/discussion_module_view.coffee
index 2e58b2c0b8..3dde9bf950 100644
--- a/common/static/coffee/src/discussion/discussion_module_view.coffee
+++ b/common/static/coffee/src/discussion/discussion_module_view.coffee
@@ -88,7 +88,7 @@ if Backbone?
if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion)
else
- $(".discussion-module").append($discussion)
+ @$el.append($discussion)
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
diff --git a/common/static/images/partially-correct-icon.png b/common/static/images/partially-correct-icon.png
new file mode 100644
index 0000000000..9ac0fd32f7
Binary files /dev/null and b/common/static/images/partially-correct-icon.png differ
diff --git a/common/static/js/capa/annotationinput.js b/common/static/js/capa/annotationinput.js
new file mode 100644
index 0000000000..4353fd262a
--- /dev/null
+++ b/common/static/js/capa/annotationinput.js
@@ -0,0 +1,97 @@
+(function () {
+ var debug = false;
+
+ var module = {
+ debug: debug,
+ inputSelector: '.annotation-input',
+ tagSelector: '.tag',
+ tagsSelector: '.tags',
+ commentSelector: 'textarea.comment',
+ valueSelector: 'input.value', // stash tag selections and comment here as a JSON string...
+
+ singleSelect: true,
+
+ init: function() {
+ var that = this;
+
+ if(this.debug) { console.log('annotation input loaded: '); }
+
+ $(this.inputSelector).each(function(index, el) {
+ if(!$(el).data('listening')) {
+ $(el).delegate(that.tagSelector, 'click', $.proxy(that.onClickTag, that));
+ $(el).delegate(that.commentSelector, 'change', $.proxy(that.onChangeComment, that));
+ $(el).data('listening', 'yes');
+ }
+ });
+ },
+ onChangeComment: function(e) {
+ var value_el = this.findValueEl(e.target);
+ var current_value = this.loadValue(value_el);
+ var target_value = $(e.target).val();
+
+ current_value.comment = target_value;
+ this.storeValue(value_el, current_value);
+ },
+ onClickTag: function(e) {
+ var target_el = e.target, target_value, target_index;
+ var value_el, current_value;
+
+ value_el = this.findValueEl(e.target);
+ current_value = this.loadValue(value_el);
+ target_value = $(e.target).data('id');
+
+ if(!$(target_el).hasClass('selected')) {
+ if(this.singleSelect) {
+ current_value.options = [target_value]
+ } else {
+ current_value.options.push(target_value);
+ }
+ } else {
+ if(this.singleSelect) {
+ current_value.options = []
+ } else {
+ target_index = current_value.options.indexOf(target_value);
+ if(target_index !== -1) {
+ current_value.options.splice(target_index, 1);
+ }
+ }
+ }
+
+ this.storeValue(value_el, current_value);
+
+ if(this.singleSelect) {
+ $(target_el).closest(this.tagsSelector)
+ .find(this.tagSelector)
+ .not(target_el)
+ .removeClass('selected')
+ }
+ $(target_el).toggleClass('selected');
+ },
+ findValueEl: function(target_el) {
+ var input_el = $(target_el).closest(this.inputSelector);
+ return $(this.valueSelector, input_el);
+ },
+ loadValue: function(value_el) {
+ var json = $(value_el).val();
+
+ var result = JSON.parse(json);
+ if(result === null) {
+ result = {};
+ }
+ if(!result.hasOwnProperty('options')) {
+ result.options = [];
+ }
+ if(!result.hasOwnProperty('comment')) {
+ result.comment = '';
+ }
+
+ return result;
+ },
+ storeValue: function(value_el, new_value) {
+ var json = JSON.stringify(new_value);
+ $(value_el).val(json);
+ }
+ }
+
+ module.init();
+}).call(this);
diff --git a/common/static/js/capa/chemical_equation_preview.js b/common/static/js/capa/chemical_equation_preview.js
index 90ce27ad11..10a6b54655 100644
--- a/common/static/js/capa/chemical_equation_preview.js
+++ b/common/static/js/capa/chemical_equation_preview.js
@@ -11,9 +11,14 @@
}
prev_id = "#" + this.id + "_preview";
- preview_div = $(prev_id)
+ preview_div = $(prev_id);
- $.get("/preview/chemcalc/", {"formula" : this.value}, create_handler(preview_div));
+ // find the closest parent problems-wrapper and use that url
+ url = $(this).closest('.problems-wrapper').data('url');
+ // grab the input id from the input
+ input_id = $(this).data('input-id')
+
+ Problem.inputAjax(url, input_id, 'preview_chemcalc', {"formula" : this.value}, create_handler(preview_div));
}
inputs = $('.chemicalequationinput input');
diff --git a/common/static/js/capa/edit-a-gene.js b/common/static/js/capa/edit-a-gene.js
index 48753e507d..bd6d10cc64 100644
--- a/common/static/js/capa/edit-a-gene.js
+++ b/common/static/js/capa/edit-a-gene.js
@@ -1,27 +1,44 @@
(function () {
var timeout = 1000;
- function initializeApplet(applet) {
- console.log("Initializing " + applet);
- waitForApplet(applet);
- }
+ waitForGenex();
- function waitForApplet(applet) {
- if (applet.isActive && applet.isActive()) {
- console.log("Applet is ready.");
- var answerStr = applet.checkAnswer();
- console.log(answerStr);
- var input = $('.editageneinput input');
- console.log(input);
- input.val(answerStr);
- } else if (timeout > 30 * 1000) {
- console.error("Applet did not load on time.");
- } else {
- console.log("Waiting for applet...");
- setTimeout(function() { waitForApplet(applet); }, timeout);
+ function waitForGenex() {
+ if (typeof(genex) !== "undefined" && genex) {
+ genex.onInjectionDone("genex");
+ }
+ else {
+ setTimeout(function() { waitForGenex(); }, timeout);
}
}
- var applets = $('.editageneinput object');
- applets.each(function(i, el) { initializeApplet(el); });
+ //NOTE:
+ // Genex uses six global functions:
+ // genexSetDNASequence (exported from GWT)
+ // genexSetClickEvent (exported from GWT)
+ // genexSetKeyEvent (exported from GWT)
+ // genexSetProblemNumber (exported from GWT)
+ //
+ // It calls genexIsReady with a deferred command when it has finished
+ // initialization and has drawn itself
+ // genexStoreAnswer(answer) is called when the GWT [Store Answer] button
+ // is clicked
+
+ genexIsReady = function() {
+ //Load DNA sequence
+ var dna_sequence = $('#dna_sequence').val();
+ genexSetDNASequence(dna_sequence);
+ //Now load mouse and keyboard handlers
+ genexSetClickEvent();
+ genexSetKeyEvent();
+ //Now load problem
+ var genex_problem_number = $('#genex_problem_number').val();
+ genexSetProblemNumber(genex_problem_number);
+ };
+ genexStoreAnswer = function(ans) {
+ var problem = $('#genex_container').parents('.problem');
+ var input_field = problem.find('input[type="hidden"][name!="dna_sequence"][name!="genex_problem_number"]');
+ input_field.val(ans);
+ };
}).call(this);
+
diff --git a/common/static/js/capa/genex/026A6180B5959B8660E084245FEE5E9E.cache.html b/common/static/js/capa/genex/026A6180B5959B8660E084245FEE5E9E.cache.html
new file mode 100644
index 0000000000..13f25ec581
--- /dev/null
+++ b/common/static/js/capa/genex/026A6180B5959B8660E084245FEE5E9E.cache.html
@@ -0,0 +1,649 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/1F433010E1134C95BF6CB43F552F3019.cache.html b/common/static/js/capa/genex/1F433010E1134C95BF6CB43F552F3019.cache.html
new file mode 100644
index 0000000000..1e99fe0f19
--- /dev/null
+++ b/common/static/js/capa/genex/1F433010E1134C95BF6CB43F552F3019.cache.html
@@ -0,0 +1,649 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html b/common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html
new file mode 100644
index 0000000000..743492768b
--- /dev/null
+++ b/common/static/js/capa/genex/2DDA730EDABB80B88A6B0DFA3AFEACA2.cache.html
@@ -0,0 +1,639 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html b/common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html
new file mode 100644
index 0000000000..4aa12e55d4
--- /dev/null
+++ b/common/static/js/capa/genex/4EEB1DCF4B30D366C27968D1B5C0BD04.cache.html
@@ -0,0 +1,651 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/5033ABB047340FB9346B622E2CC7107D.cache.html b/common/static/js/capa/genex/5033ABB047340FB9346B622E2CC7107D.cache.html
new file mode 100644
index 0000000000..167a193adb
--- /dev/null
+++ b/common/static/js/capa/genex/5033ABB047340FB9346B622E2CC7107D.cache.html
@@ -0,0 +1,625 @@
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/DF3D3A7FAEE63D711CF2D95BDB3F538C.cache.html b/common/static/js/capa/genex/DF3D3A7FAEE63D711CF2D95BDB3F538C.cache.html
new file mode 100644
index 0000000000..913b90be20
--- /dev/null
+++ b/common/static/js/capa/genex/DF3D3A7FAEE63D711CF2D95BDB3F538C.cache.html
@@ -0,0 +1,639 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/static/js/capa/genex/clear.cache.gif b/common/static/js/capa/genex/clear.cache.gif
new file mode 100644
index 0000000000..e565824aaf
Binary files /dev/null and b/common/static/js/capa/genex/clear.cache.gif differ
diff --git a/common/static/js/capa/genex/genex.css b/common/static/js/capa/genex/genex.css
new file mode 100644
index 0000000000..a05f31110b
--- /dev/null
+++ b/common/static/js/capa/genex/genex.css
@@ -0,0 +1,109 @@
+.genex-button {
+ margin-right: -8px;
+ height: 40px !important;
+}
+
+.genex-label {
+ /*font: normal normal normal 10pt/normal 'Open Sans', Verdana, Geneva, sans-serif !important;*/
+ /*padding: 4px 0px 0px 10px !important;*/
+ font-family: sans-serif !important;
+ font-size: 13px !important;
+ font-style: normal !important;
+ font-variant: normal !important;
+ font-weight: bold !important;
+ padding-top: 6px !important;
+ margin-left: 18px;
+}
+
+.gwt-HTML {
+ cursor: default;
+ overflow-x: auto !important;
+ overflow-y: auto !important;
+ background-color: rgb(248, 248, 248) !important;
+}
+
+.genex-scrollpanel {
+ word-wrap: normal !important;
+ white-space: pre !important;
+}
+
+pre, #dna-strand {
+ font-family: 'courier new', courier !important;
+ font-size: 13px !important;
+ font-style: normal !important;
+ font-variant: normal !important;
+ font-weight: normal !important;
+ border-style: none !important;
+ background-color: rgb(248, 248, 248) !important;
+ word-wrap: normal !important;
+ white-space: pre !important;
+ overflow-x: visible !important;
+ overflow-y: visible !important;
+}
+
+.gwt-DialogBox .Caption {
+ background: #F1F1F1;
+ padding: 4px 8px 4px 4px;
+ cursor: default;
+ font-family: Arial Unicode MS, Arial, sans-serif;
+ font-weight: bold;
+ border-bottom: 1px solid #bbbbbb;
+ border-top: 1px solid #D2D2D2;
+}
+.gwt-DialogBox .dialogContent {
+}
+.gwt-DialogBox .dialogMiddleCenter {
+ padding: 3px;
+ background: white;
+}
+.gwt-DialogBox .dialogBottomCenter {
+}
+.gwt-DialogBox .dialogMiddleLeft {
+}
+.gwt-DialogBox .dialogMiddleRight {
+}
+.gwt-DialogBox .dialogTopLeftInner {
+ width: 10px;
+ height: 8px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogTopRightInner {
+ width: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogBottomLeftInner {
+ width: 10px;
+ height: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogBottomRightInner {
+ width: 12px;
+ height: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogTopLeft {
+}
+.gwt-DialogBox .dialogTopRight {
+}
+.gwt-DialogBox .dialogBottomLeft {
+}
+.gwt-DialogBox .dialogBottomRight {
+}
+* html .gwt-DialogBox .dialogTopLeftInner {
+ width: 10px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogTopRightInner {
+ width: 12px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogBottomLeftInner {
+ width: 10px;
+ height: 12px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogBottomRightInner {
+ width: 12px;
+ height: 12px;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/common/static/js/capa/genex/genex.nocache.js b/common/static/js/capa/genex/genex.nocache.js
new file mode 100644
index 0000000000..07da038234
--- /dev/null
+++ b/common/static/js/capa/genex/genex.nocache.js
@@ -0,0 +1,18 @@
+function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='026A6180B5959B8660E084245FEE5E9E',Rb='1F433010E1134C95BF6CB43F552F3019',Sb='2DDA730EDABB80B88A6B0DFA3AFEACA2',Tb='4EEB1DCF4B30D366C27968D1B5C0BD04',Ub='5033ABB047340FB9346B622E2CC7107D',Wb=':',pb='::',dc='
+
+
This html file is for Development Mode support.
+