Merge branch 'master' into feature/diana/close-oe-problems

This commit is contained in:
Diana Huang
2013-02-05 09:12:08 -05:00
41 changed files with 1815 additions and 708 deletions

View File

@@ -40,6 +40,11 @@ def get_logger_config(log_dir,
if console_loglevel is None or console_loglevel not in LOG_LEVELS:
console_loglevel = 'DEBUG' if debug else 'INFO'
if service_variant is None:
# default to a blank string so that if SERVICE_VARIANT is not
# set we will not log to a sub directory
service_variant = ''
hostname = platform.node().split(".")[0]
syslog_format = ("[service_variant={service_variant}]"
"[%(name)s][env:{logging_env}] %(levelname)s "

View File

@@ -20,6 +20,7 @@ setup(
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor",
"combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor",
"conditional = xmodule.conditional_module:ConditionalDescriptor",
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
@@ -28,6 +29,7 @@ setup(
"error = xmodule.error_module:ErrorDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",

View File

@@ -2,6 +2,7 @@ import cgi
import datetime
import dateutil
import dateutil.parser
import hashlib
import json
import logging
import traceback
@@ -25,6 +26,22 @@ log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
# Generated this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20
def randomization_bin(seed, problem_id):
"""
Pick a randomization bin for the problem given the user's seed and a problem id.
We do this because we only want e.g. 20 randomizations of a problem to make analytics
interesting. To avoid having sets of students that always get the same problems,
we'll combine the system's per-student seed with the problem id in picking the bin.
"""
h = hashlib.sha1()
h.update(str(seed))
h.update(str(problem_id))
# get the first few digits of the hash, convert to an int, then mod.
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
def only_one(lst, default="", process=lambda x: x):
"""
@@ -138,13 +155,9 @@ class CapaModule(XModule):
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
# TODO: This line is badly broken:
# (1) We're passing student ID to xmodule.
# (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
# to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
# - analytics really needs small number of bins.
self.seed = system.id
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# see comment on randomization_bin
self.seed = randomization_bin(system.seed, self.location.url)
else:
self.seed = None
@@ -389,38 +402,54 @@ class CapaModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
def is_past_due(self):
"""
Is it now past this problem's due date, including grace period?
"""
return (self.close_date is not None and
datetime.datetime.utcnow() > self.close_date)
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
return True
if self.close_date is not None and datetime.datetime.utcnow() > self.close_date:
if self.is_past_due():
return True
return False
def is_completed(self):
# used by conditional module
# return self.answer_available()
return self.lcp.done
def is_attempted(self):
# used by conditional module
return self.attempts > 0
def answer_available(self):
''' Is the user allowed to see an answer?
'''
Is the user allowed to see an answer?
'''
if self.show_answer == '':
return False
if self.show_answer == "never":
elif self.show_answer == "never":
return False
# Admins can see the answer, unless the problem explicitly prevents it
if self.system.user_is_staff:
elif self.system.user_is_staff:
# This is after the 'never' check because admins can see the answer
# unless the problem explicitly prevents it
return True
if self.show_answer == 'attempted':
elif self.show_answer == 'attempted':
return self.attempts > 0
if self.show_answer == 'answered':
elif self.show_answer == 'answered':
# NOTE: this is slightly different from 'attempted' -- resetting the problems
# makes lcp.done False, but leaves attempts unchanged.
return self.lcp.done
if self.show_answer == 'closed':
elif self.show_answer == 'closed':
return self.closed()
if self.show_answer == 'always':
elif self.show_answer == 'past_due':
return self.is_past_due()
elif self.show_answer == 'always':
return True
return False
@@ -669,18 +698,18 @@ class CapaDescriptor(RawDescriptor):
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
def get_context(self):
_context = RawDescriptor.get_context(self)
_context.update({'markdown': self.metadata.get('markdown', '')})
return _context
@property
def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor,self).editable_metadata_fields
if 'markdown' in subset:
subset.remove('markdown')
subset.remove('markdown')
return subset

View File

@@ -0,0 +1,141 @@
import json
import logging
from xmodule.x_module import XModule
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from pkg_resources import resource_string
log = logging.getLogger('mitx.' + __name__)
class ConditionalModule(XModule):
'''
Blocks child module from showing unless certain conditions are met.
Example:
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
<video url_name="secret_video" />
</conditional>
<conditional condition="require_attempted" required="tag/url_name1&tag/url_name2">
<video url_name="secret_video" />
</conditional>
'''
js = {'coffee': [resource_string(__name__, 'js/src/conditional/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "Conditional"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
"""
In addition to the normal XModule init, provide:
self.condition = string describing condition required
"""
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
self.contents = None
self.condition = self.metadata.get('condition','')
#log.debug('conditional module required=%s' % self.required_modules_list)
def _get_required_modules(self):
self.required_modules = []
for descriptor in self.descriptor.get_required_module_descriptors():
module = self.system.get_module(descriptor)
self.required_modules.append(module)
#log.debug('required_modules=%s' % (self.required_modules))
def is_condition_satisfied(self):
self._get_required_modules()
if self.condition=='require_completed':
# all required modules must be completed, as determined by
# the modules .is_completed() method
for module in self.required_modules:
#log.debug('in is_condition_satisfied; student_answers=%s' % module.lcp.student_answers)
#log.debug('in is_condition_satisfied; instance_state=%s' % module.instance_state)
if not hasattr(module, 'is_completed'):
raise Exception('Error in conditional module: required module %s has no .is_completed() method' % module)
if not module.is_completed():
log.debug('conditional module: %s not completed' % module)
return False
else:
log.debug('conditional module: %s IS completed' % module)
return True
elif self.condition=='require_attempted':
# all required modules must be attempted, as determined by
# the modules .is_attempted() method
for module in self.required_modules:
if not hasattr(module, 'is_attempted'):
raise Exception('Error in conditional module: required module %s has no .is_attempted() method' % module)
if not module.is_attempted():
log.debug('conditional module: %s not attempted' % module)
return False
else:
log.debug('conditional module: %s IS attempted' % module)
return True
else:
raise Exception('Error in conditional module: unknown condition "%s"' % self.condition)
return True
def get_html(self):
self.is_condition_satisfied()
return self.system.render_template('conditional_ajax.html', {
'element_id': self.location.html_id(),
'id': self.id,
'ajax_url': self.system.ajax_url,
})
def handle_ajax(self, dispatch, post):
'''
This is called by courseware.module_render, to handle an AJAX call.
'''
#log.debug('conditional_module handle_ajax: dispatch=%s' % dispatch)
if not self.is_condition_satisfied():
context = {'module': self}
html = self.system.render_template('conditional_module.html', context)
return json.dumps({'html': html})
if self.contents is None:
self.contents = [child.get_html() for child in self.get_display_items()]
# for now, just deal with one child
html = self.contents[0]
return json.dumps({'html': html})
class ConditionalDescriptor(SequenceDescriptor):
module_class = ConditionalModule
filename_extension = "xml"
stores_state = True
has_score = False
def __init__(self, *args, **kwargs):
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')]
self.required_module_locations = []
for (tag, name) in required_module_list:
loc = self.location.dict()
loc['category'] = tag
loc['name'] = name
self.required_module_locations.append(Location(loc))
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
def get_required_module_descriptors(self):
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
not children of this module"""
return [self.system.load_item(loc) for loc in self.required_module_locations]

View File

@@ -442,12 +442,13 @@ section.open-ended-child {
margin: 10px;
}
span.short-form-response {
padding: 9px;
div.short-form-response {
background: #F6F6F6;
border: 1px solid #ddd;
border-top: 0;
margin-bottom: 20px;
overflow-y: auto;
height: 200px;
@include clearfix;
}

View File

@@ -329,7 +329,7 @@ class @CombinedOpenEnded
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
if response.state == "done" or response.state=="post_assessment"
delete window.queuePollerID
@reload
location.reload()
else
window.queuePollerID = window.setTimeout(@poll, 10000)
@@ -351,7 +351,7 @@ class @CombinedOpenEnded
answer_id = @answer_area.attr('id')
answer_val = @answer_area.val()
new_text = ''
new_text = "<span class='#{answer_class}' id='#{answer_id}'>#{answer_val}</span>"
new_text = "<div class='#{answer_class}' id='#{answer_id}'>#{answer_val}</div>"
@answer_area.replaceWith(new_text)
# wrap this so that it can be mocked

View File

@@ -0,0 +1,26 @@
class @Conditional
constructor: (element) ->
@el = $(element).find('.conditional-wrapper')
@id = @el.data('problem-id')
@element_id = @el.attr('id')
@url = @el.data('url')
@render()
$: (selector) ->
$(selector, @el)
updateProgress: (response) =>
if response.progress_changed
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
render: (content) ->
if content
@el.html(content)
XModule.loadModules(@el)
else
$.postWithPrefix "#{@url}/conditional_get", (response) =>
@el.html(response.html)
XModule.loadModules(@el)

View File

@@ -50,7 +50,7 @@ class @HTMLEditingDescriptor
})
@showingVisualEditor = true
@element.on('click', '.editor-tabs .tab', @onSwitchEditor)
@element.on('click', '.editor-tabs .tab', this, @onSwitchEditor)
@setupTinyMCE: (ed) ->
ed.addButton('wrapAsCode', {
@@ -71,15 +71,18 @@ class @HTMLEditingDescriptor
e.preventDefault();
if not $(e.currentTarget).hasClass('current')
$('.editor-tabs .current', @element).removeClass('current')
element = e.data.element
$(e.currentTarget).addClass('current')
$('table.mceToolbar', @element).toggleClass(HTMLEditingDescriptor.isInactiveClass)
$(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass)
$(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass)
visualEditor = @getVisualEditor()
visualEditor = @getVisualEditor(element)
if $(e.currentTarget).attr('data-tab') is 'visual'
$(element).find('.html-tab').removeClass('current')
@showVisualEditor(visualEditor)
else
$(element).find('.visual-tab').removeClass('current')
@showAdvancedEditor(visualEditor)
# Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
@@ -104,17 +107,17 @@ class @HTMLEditingDescriptor
focusVisualEditor: (visualEditor) ->
visualEditor.focus()
getVisualEditor: ->
getVisualEditor: (element) ->
###
Returns the instance of TinyMCE.
This is different from the textarea that exists in the HTML template (@tiny_mce_textarea.
###
return tinyMCE.get($('.tiny-mce', this.element).attr('id'))
return tinyMCE.get($(element).find('.tiny-mce').attr('id'))
save: ->
@element.off('click', '.editor-tabs .tab', @onSwitchEditor)
text = @advanced_editor.getValue()
visualEditor = @getVisualEditor()
visualEditor = @getVisualEditor(@element)
if @showingVisualEditor and visualEditor.isDirty()
text = visualEditor.getContent({no_events: 1})
data: text

View File

@@ -0,0 +1,121 @@
import json
import logging
import random
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from pkg_resources import resource_string
log = logging.getLogger('mitx.' + __name__)
class RandomizeModule(XModule):
"""
Chooses a random child module. Chooses the same one every time for each student.
Example:
<randomize>
<problem url_name="problem1" />
<problem url_name="problem2" />
<problem url_name="problem3" />
</randomize>
User notes:
- If you're randomizing amongst graded modules, each of them MUST be worth the same
number of points. Otherwise, the earth will be overrun by monsters from the
deeps. You have been warned.
Technical notes:
- There is more dark magic in this code than I'd like. The whole varying-children +
grading interaction is a tangle between super and subclasses of descriptors and
modules.
"""
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
# NOTE: calling self.get_children() creates a circular reference--
# it calls get_child_descriptors() internally, but that doesn't work until
# we've picked a choice
num_choices = len(self.descriptor.get_children())
self.choice = None
if instance_state is not None:
state = json.loads(instance_state)
self.choice = state.get('choice', None)
if self.choice > num_choices:
# Oops. Children changed. Reset.
self.choice = None
if self.choice is None:
# choose one based on the system seed, or randomly if that's not available
if num_choices > 0:
if system.seed is not None:
self.choice = system.seed % num_choices
else:
self.choice = random.randrange(0, num_choices)
if self.choice is not None:
self.child_descriptor = self.descriptor.get_children()[self.choice]
# Now get_children() should return a list with one element
log.debug("children of randomize module (should be only 1): %s",
self.get_children())
self.child = self.get_children()[0]
else:
self.child_descriptor = None
self.child = None
def get_instance_state(self):
return json.dumps({'choice': self.choice})
def get_child_descriptors(self):
"""
For grading--return just the chosen child.
"""
if self.child_descriptor is None:
return []
return [self.child_descriptor]
def get_html(self):
if self.child is None:
# raise error instead? In fact, could complain on descriptor load...
return "<div>Nothing to randomize between</div>"
return self.child.get_html()
def get_icon_class(self):
return self.child.get_icon_class() if self.child else 'other'
class RandomizeDescriptor(SequenceDescriptor):
# the editing interface can be the same as for sequences -- just a container
module_class = RandomizeModule
filename_extension = "xml"
stores_state = True
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('randomize')
for child in self.get_children():
xml_object.append(
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object
def has_dynamic_children(self):
"""
Grading needs to know that only one of the children is actually "real". This
makes it use module.get_child_descriptors().
"""
return True

View File

@@ -26,7 +26,7 @@ test_system = ModuleSystem(
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
user=Mock(),
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},

View File

@@ -0,0 +1,215 @@
import datetime
import json
from mock import Mock
from pprint import pprint
import unittest
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
from . import test_system
class CapaFactory(object):
"""
A helper class to create problem modules with various parameters for testing.
"""
sample_problem_xml = """<?xml version="1.0"?>
<problem>
<text>
<p>What is pi, to two decimal placs?</p>
</text>
<numericalresponse answer="3.14">
<textline math="1" size="30"/>
</numericalresponse>
</problem>
"""
num = 0
@staticmethod
def next_num():
CapaFactory.num += 1
return CapaFactory.num
@staticmethod
def create(graceperiod=None,
due=None,
max_attempts=None,
showanswer=None,
rerandomize=None,
force_save_button=None,
attempts=None,
problem_state=None,
):
"""
All parameters are optional, and are added to the created problem if specified.
Arguments:
graceperiod:
due:
max_attempts:
showanswer:
force_save_button:
rerandomize: all strings, as specified in the policy for the problem
problem_state: a dict to to be serialized into the instance_state of the
module.
attempts: also added to instance state. Will be converted to an int.
"""
definition = {'data': CapaFactory.sample_problem_xml,}
location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CapaFactory.next_num())])
metadata = {}
if graceperiod is not None:
metadata['graceperiod'] = graceperiod
if due is not None:
metadata['due'] = due
if max_attempts is not None:
metadata['attempts'] = max_attempts
if showanswer is not None:
metadata['showanswer'] = showanswer
if force_save_button is not None:
metadata['force_save_button'] = force_save_button
if rerandomize is not None:
metadata['rerandomize'] = rerandomize
descriptor = Mock(weight="1")
instance_state_dict = {}
if problem_state is not None:
instance_state_dict = problem_state
if attempts is not None:
# converting to int here because I keep putting "0" and "1" in the tests
# since everything else is a string.
instance_state_dict['attempts'] = int(attempts)
if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict)
else:
instance_state = None
module = CapaModule(test_system, location,
definition, descriptor,
instance_state, None, metadata=metadata)
return module
class CapaModuleTest(unittest.TestCase):
def setUp(self):
now = datetime.datetime.now()
day_delta = datetime.timedelta(days=1)
self.yesterday_str = str(now - day_delta)
self.today_str = str(now)
self.tomorrow_str = str(now + day_delta)
# in the capa grace period format, not in time delta format
self.two_day_delta_str = "2 days"
def test_import(self):
module = CapaFactory.create()
self.assertEqual(module.get_score()['score'], 0)
other_module = CapaFactory.create()
self.assertEqual(module.get_score()['score'], 0)
self.assertNotEqual(module.url_name, other_module.url_name,
"Factory should be creating unique names for each problem")
def test_showanswer_default(self):
"""
Make sure the show answer logic does the right thing.
"""
# default, no due date, showanswer 'closed', so problem is open, and show_answer
# not visible.
problem = CapaFactory.create()
self.assertFalse(problem.answer_available())
def test_showanswer_attempted(self):
problem = CapaFactory.create(showanswer='attempted')
self.assertFalse(problem.answer_available())
problem.attempts = 1
self.assertTrue(problem.answer_available())
def test_showanswer_closed(self):
# can see after attempts used up, even with due date in the future
used_all_attempts = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="1",
due=self.tomorrow_str)
self.assertTrue(used_all_attempts.answer_available())
# can see after due date
after_due_date = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.yesterday_str)
self.assertTrue(after_due_date.answer_available())
# can't see because attempts left
attempts_left_open = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.tomorrow_str)
self.assertFalse(attempts_left_open.answer_available())
# Can't see because grace period hasn't expired
still_in_grace = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertFalse(still_in_grace.answer_available())
def test_showanswer_past_due(self):
"""
With showanswer="past_due" should only show answer after the problem is closed
for everyone--e.g. after due date + grace period.
"""
# can see after attempts used up, even with due date in the future
used_all_attempts = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="1",
due=self.tomorrow_str)
self.assertFalse(used_all_attempts.answer_available())
# can see after due date
past_due_date = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="0",
due=self.yesterday_str)
self.assertTrue(past_due_date.answer_available())
# can't see because attempts left
attempts_left_open = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="0",
due=self.tomorrow_str)
self.assertFalse(attempts_left_open.answer_available())
# Can't see because grace period hasn't expired, even though have no more
# attempts.
still_in_grace = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="1",
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertFalse(still_in_grace.answer_available())

View File

@@ -0,0 +1,119 @@
import json
from path import path
import unittest
from fs.memoryfs import MemoryFS
from lxml import etree
from mock import Mock, patch
from collections import defaultdict
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
from .test_export import DATA_DIR
ORG = 'test_org'
COURSE = 'conditional' # name of directory with course data
from . import test_system
class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
course_id = "/".join([ORG, COURSE, 'test_run'])
course_dir = "test_dir"
policy = {}
error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__(
xmlstore,
course_id,
course_dir,
policy,
error_tracker,
parent_tracker,
load_error_modules=load_error_modules,
)
def render_template(self, template, context):
raise Exception("Shouldn't be called")
class ConditionalModuleTest(unittest.TestCase):
@staticmethod
def get_system(load_error_modules=True):
'''Get a dummy system'''
return DummySystem(load_error_modules)
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
self.modulestore = modulestore
self.assertEquals(len(courses), 1)
return courses[0]
def test_conditional_module(self):
"""Make sure that conditional module works"""
print "Starting import"
course = self.get_course('conditional')
print "Course: ", course
print "id: ", course.id
instance_states = dict(problem=None)
shared_state = None
def inner_get_module(descriptor):
if isinstance(descriptor, Location):
location = descriptor
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location
instance_state = instance_states.get(location.category,None)
print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional","condone"])
module = inner_get_module(location)
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
print "module: ", module
html = module.get_html()
print "html type: ", type(html)
print "html: ", html
html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}"
self.assertEqual(html, html_expect)
gdi = module.get_display_items()
print "gdi=", gdi
ajax = json.loads(module.handle_ajax('',''))
self.assertTrue('xmodule.conditional_module' in ajax['html'])
print "ajax: ", ajax
# now change state of the capa problem to make it completed
instance_states['problem'] = json.dumps({'attempts':1})
ajax = json.loads(module.handle_ajax('',''))
self.assertTrue('This is a secret' in ajax['html'])
print "post-attempt ajax: ", ajax

View File

@@ -0,0 +1,55 @@
import unittest
from time import strptime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
ORG = 'test_org'
COURSE = 'test_course'
START = '2013-01-01T01:00:00'
from test_course_module import DummySystem as DummyImportSystem
from . import test_system
class RandomizeModuleTestCase(unittest.TestCase):
"""Make sure the randomize module works"""
@staticmethod
def get_dummy_course(start):
"""Get a dummy course"""
system = DummyImportSystem(load_error_modules=True)
def to_attrb(n, v):
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
start_xml = '''
<course org="{org}" course="{course}"
graceperiod="1 day" url_name="test"
start="{start}"
>
<chapter url="hi" url_name="ch" display_name="CH">
<randomize url_name="my_randomize">
<html url_name="a" display_name="A">Two houses, ...</html>
<html url_name="b" display_name="B">Three houses, ...</html>
</randomize>
</chapter>
</course>
'''.format(org=ORG, course=COURSE, start=start)
return system.process_xml(start_xml)
def test_import(self):
"""
Just make sure descriptor loads without error
"""
descriptor = self.get_dummy_course(START)
# TODO: add tests that create a module and check. Passing state is a good way to
# check that child access works...

View File

@@ -48,3 +48,5 @@ class VerticalDescriptor(SequenceDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
# like verticals will get exported as sequentials...

View File

@@ -585,6 +585,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._inherited_metadata.add(attr)
self.metadata[attr] = metadata[attr]
def get_required_module_descriptors(self):
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
not children of this module"""
return []
def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of
this module"""