diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py
index 571526f915..037e5b0d9c 100644
--- a/common/lib/capa/capa/chem/tests.py
+++ b/common/lib/capa/capa/chem/tests.py
@@ -277,7 +277,6 @@ class Test_Render_Equations(unittest.TestCase):
def test_render9(self):
s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
- #import ipdb; ipdb.set_trace()
out = render_to_html(s)
correct = u'5[Ni(NH3)4]2++5⁄2SO42-'
log(out + ' ------- ' + correct, 'html')
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 048804fb60..bf49987511 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -28,6 +28,7 @@ setup(
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
+ "poll_question = xmodule.poll_module:PollDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
@@ -44,8 +45,8 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor",
- "poll = xmodule.poll_module:PollDescriptor",
- "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
- ]
+ "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor,
+ "wrapper = xmodule.wrapper_module:WrapperDescriptor",
+ ],
}
)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 92742203c4..b82a8c7113 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -521,7 +521,6 @@ class CapaModule(XModule):
answers = self.make_dict_of_responses(get)
event_info['answers'] = convert_files_to_filenames(answers)
-
# Too late. Cannot submit
if self.closed():
event_info['failure'] = 'closed'
diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py
index 11d2a58231..6a0388efed 100644
--- a/common/lib/xmodule/xmodule/conditional_module.py
+++ b/common/lib/xmodule/xmodule/conditional_module.py
@@ -1,127 +1,129 @@
+"""Conditional module is the xmodule, which you can use for disabling
+some xmodules by conditions.
+"""
+
import json
import logging
+from lxml import etree
+from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
-from xblock.core import String, Scope
+from xblock.core import String, Scope, List
+from xmodule.modulestore.exceptions import ItemNotFoundError
-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:
-
+
+
-
-
-
+ TODO string comparison
+ multiple answer for every poll
- '''
+ """
- js = {'coffee': [resource_string(__name__, 'js/src/conditional/display.coffee'),
+ js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.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')]}
- condition = String(help="Condition for this module", default='', scope=Scope.settings)
+ contents = String(scope=Scope.content)
- def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
- """
- In addition to the normal XModule init, provide:
+ # Map
+ # key:
+ # value:
+ conditions_map = {
+ 'poll_answer': 'poll_answer', # poll_question attr
+ 'compeleted': 'is_competed', # capa_problem attr
+ 'attempted': 'is_attempted', # capa_problem attr
+ 'voted': 'voted' # poll_question attr
+ }
- self.condition = string describing condition required
-
- """
- XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
- self.contents = None
- self._get_required_modules()
- children = self.get_display_items()
- if children:
- self.icon_class = children[0].get_icon_class()
- #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 _get_condition(self):
+ # Get first valid condition.
+ for xml_attr, attr_name in self.conditions_map.iteritems():
+ xml_value = self.descriptor.xml_attributes.get(xml_attr)
+ if xml_value:
+ return xml_value, attr_name
+ raise Exception('Error in conditional module: unknown condition "%s"'
+ % xml_attr)
def is_condition_satisfied(self):
- self._get_required_modules()
+ self.required_modules = [self.system.get_module(descriptor.location) for
+ descriptor in self.descriptor.get_required_module_descriptors()]
- 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)
+ xml_value, attr_name = self._get_condition()
- return True
+ if xml_value and self.required_modules:
+ for module in self.required_modules:
+ if not hasattr(module, attr_name):
+ raise Exception('Error in conditional module: \
+ required module {module} has no {module_attr}'.format(
+ module=module, module_attr=attr_name))
+
+ attr = getattr(module, attr_name)
+ if callable(attr):
+ attr = attr()
+
+ if xml_value != str(attr):
+ break
+ else:
+ return True
+ return False
def get_html(self):
- self.is_condition_satisfied()
+ # Calculate html ids of dependencies
+ self.required_html_ids = [descriptor.location.html_id() for
+ descriptor in self.descriptor.get_required_module_descriptors()]
+
return self.system.render_template('conditional_ajax.html', {
'element_id': self.location.html_id(),
'id': self.id,
'ajax_url': self.system.ajax_url,
+ 'depends': ';'.join(self.required_html_ids)
})
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)
-
+ """This is called by courseware.moduleodule_render, to handle
+ an AJAX call.
+ """
if not self.is_condition_satisfied():
- context = {'module': self}
- html = self.system.render_template('conditional_module.html', context)
- return json.dumps({'html': html})
+ message = self.descriptor.xml_attributes.get('message')
+ context = {'module': self,
+ 'message': message}
+ html = self.system.render_template('conditional_module.html',
+ context)
+ return json.dumps({'html': [html], 'passed': False,
+ 'message': bool(message)})
if self.contents is None:
- self.contents = [child.get_html() for child in self.get_display_items()]
+ self.contents = [self.system.get_module(child_descriptor.location
+ ).get_html()
+ for child_descriptor in self.descriptor.get_children()]
- # for now, just deal with one child
- html = self.contents[0]
-
- return json.dumps({'html': html})
+ html = self.contents
+ return json.dumps({'html': html, 'passed': True})
class ConditionalDescriptor(SequenceDescriptor):
+ """Descriptor for conditional xmodule."""
+ _tag_name = 'conditional'
+
module_class = ConditionalModule
filename_extension = "xml"
@@ -129,28 +131,66 @@ class ConditionalDescriptor(SequenceDescriptor):
stores_state = True
has_score = False
- required = String(help="List of required xmodule locations, separated by &", default='', scope=Scope.settings)
+ show_tag_list = List(help="Poll answers", scope=Scope.content)
- def __init__(self, *args, **kwargs):
- super(ConditionalDescriptor, self).__init__(*args, **kwargs)
-
- required_module_list = [tuple(x.split('/', 1)) for x in self.required.split('&')]
- self.required_module_locations = []
- for rm in required_module_list:
- try:
- (tag, name) = rm
- except Exception as err:
- msg = "Specification of required module in conditional is broken: %s" % self.required
- log.warning(msg)
- self.system.error_tracker(msg)
- continue
- 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)
+ @staticmethod
+ def parse_sources(xml_element, system, return_descriptor=False):
+ """Parse xml_element 'sources' attr and:
+ if return_descriptor=True - return list of descriptors
+ if return_descriptor=False - return list of lcoations
+ """
+ result = []
+ sources = xml_element.get('sources')
+ if sources:
+ locations = [location.strip() for location in sources.split(';')]
+ for location in locations:
+ # Check valid location url.
+ if Location.is_valid(location):
+ try:
+ descriptor = system.load_item(location)
+ if return_descriptor:
+ result.append(descriptor)
+ else:
+ result.append(location)
+ except ItemNotFoundError:
+ log.exception("Invalid module by location.")
+ return result
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]
+ """Returns a list of XModuleDescritpor instances upon
+ which this module depends.
+ """
+ return ConditionalDescriptor.parse_sources(
+ self.xml_attributes, self.system, True)
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ children = []
+ show_tag_list = []
+ for child in xml_object:
+ if child.tag == 'show':
+ location = ConditionalDescriptor.parse_sources(
+ child, system)
+ children.extend(location)
+ show_tag_list.extend(location)
+ else:
+ try:
+ descriptor = system.process_xml(etree.tostring(child))
+ module_url = descriptor.location.url()
+ children.append(module_url)
+ except:
+ log.exception("Unable to load child when parsing Conditional.")
+ return {'show_tag_list': show_tag_list}, children
+
+ def definition_to_xml(self, resource_fs):
+ xml_object = etree.Element(self._tag_name)
+ for child in self.get_children():
+ location = str(child.location)
+ if location in self.show_tag_list:
+ show_str = '<{tag_name} sources="{sources}" />'.format(
+ tag_name='show', sources=location)
+ xml_object.append(etree.fromstring(show_str))
+ else:
+ xml_object.append(
+ etree.fromstring(child.export_to_xml(resource_fs)))
+ return xml_object
diff --git a/common/lib/xmodule/xmodule/css/poll/display.scss b/common/lib/xmodule/xmodule/css/poll/display.scss
new file mode 100644
index 0000000000..53192823be
--- /dev/null
+++ b/common/lib/xmodule/xmodule/css/poll/display.scss
@@ -0,0 +1,226 @@
+section.poll_question {
+ @media print {
+ display: block;
+ width: auto;
+ padding: 0;
+
+ canvas, img {
+ page-break-inside: avoid;
+ }
+ }
+
+ .inline {
+ display: inline;
+ }
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 15px;
+ color: #fe57a1;
+ font-size: 1.9em;
+
+ &.problem-header {
+ section.staff {
+ margin-top: 30px;
+ font-size: 80%;
+ }
+ }
+
+ @media print {
+ display: block;
+ width: auto;
+ border-right: 0;
+ }
+ }
+
+ p {
+ text-align: justify;
+ font-weight: bold;
+ }
+
+ .poll_answer {
+ margin-bottom: 20px;
+
+ &.short {
+ clear: both;
+ }
+
+ .question {
+ height: auto;
+ clear: both;
+ min-height: 30px;
+
+ &.short {
+ clear: none;
+ width: 30%;
+ display: inline;
+ float: left;
+ }
+
+ .button {
+ -webkit-appearance: none;
+ -webkit-background-clip: padding-box;
+ -webkit-border-image: none;
+ -webkit-box-align: center;
+ -webkit-box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
+ -webkit-font-smoothing: antialiased;
+ -webkit-rtl-ordering: logical;
+ -webkit-user-select: text;
+ -webkit-writing-mode: horizontal-tb;
+ background-clip: padding-box;
+ background-color: rgb(238, 238, 238);
+ background-image: -webkit-linear-gradient(top, rgb(238, 238, 238), rgb(210, 210, 210));
+ border-bottom-color: rgb(202, 202, 202);
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ border-left-color: rgb(202, 202, 202);
+ border-left-style: solid;
+ border-left-width: 1px;
+ border-right-color: rgb(202, 202, 202);
+ border-right-style: solid;
+ border-right-width: 1px;
+ border-top-color: rgb(202, 202, 202);
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ border-top-style: solid;
+ border-top-width: 1px;
+ box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
+ box-sizing: border-box;
+ color: rgb(51, 51, 51);
+ cursor: pointer;
+
+ /* display: inline-block; */
+ display: inline;
+ float: left;
+
+ font-family: 'Open Sans', Verdana, Geneva, sans-serif;
+ font-size: 13px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: bold;
+
+ letter-spacing: normal;
+ line-height: 25.59375px;
+ margin-bottom: 15px;
+ margin: 0px;
+ padding: 0px;
+ text-align: center;
+ text-decoration: none;
+ text-indent: 0px;
+ text-shadow: rgb(248, 248, 248) 0px 1px 0px;
+ text-transform: none;
+ vertical-align: top;
+ white-space: pre-line;
+
+ width: 25px;
+ height: 25px;
+
+ word-spacing: 0px;
+ writing-mode: lr-tb;
+ }
+ .button.answered {
+ -webkit-box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
+ background-color: rgb(29, 157, 217);
+ background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
+ border-bottom-color: rgb(13, 114, 162);
+ border-left-color: rgb(13, 114, 162);
+ border-right-color: rgb(13, 114, 162);
+ border-top-color: rgb(13, 114, 162);
+ box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
+ color: rgb(255, 255, 255);
+ text-shadow: rgb(7, 103, 148) 0px 1px 0px;
+ }
+
+ .text {
+ display: inline;
+ float: left;
+ width: 80%;
+ text-align: left;
+ min-height: 30px;
+ margin-left: 20px;
+ height: auto;
+ margin-bottom: 20px;
+ cursor: pointer;
+
+ &.short {
+ width: 100px;
+ }
+ }
+ }
+
+ .stats {
+ min-height: 40px;
+ margin-top: 20px;
+ clear: both;
+
+ &.short {
+ margin-top: 0;
+ clear: none;
+ display: inline;
+ float: right;
+ width: 70%;
+ }
+
+ .bar {
+ width: 75%;
+ height: 20px;
+ border: 1px solid black;
+ display: inline;
+ float: left;
+ margin-right: 10px;
+
+ &.short {
+ width: 65%;
+ height: 20px;
+ margin-top: 3px;
+ }
+
+ .percent {
+ background-color: gray;
+ width: 0px;
+ height: 20px;
+
+ &.short { }
+ }
+ }
+
+ .number {
+ width: 80px;
+ display: inline;
+ float: right;
+ height: 28px;
+ text-align: right;
+
+ &.short {
+ width: 120px;
+ height: auto;
+ }
+ }
+ }
+ }
+
+ .poll_answer.answered {
+ -webkit-box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
+ background-color: rgb(29, 157, 217);
+ background-image: -webkit-linear-gradient(top, rgb(29, 157, 217), rgb(14, 124, 176));
+ border-bottom-color: rgb(13, 114, 162);
+ border-left-color: rgb(13, 114, 162);
+ border-right-color: rgb(13, 114, 162);
+ border-top-color: rgb(13, 114, 162);
+ box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
+ color: rgb(255, 255, 255);
+ text-shadow: rgb(7, 103, 148) 0px 1px 0px;
+ }
+
+ .graph_answer {
+ display: none;
+ clear: both;
+ width: 400px;
+ height: 400px;
+ margin-top: 30px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
diff --git a/common/lib/xmodule/xmodule/css/wrapper/display.scss b/common/lib/xmodule/xmodule/css/wrapper/display.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore
index 03534687ca..139597f9cb 100644
--- a/common/lib/xmodule/xmodule/js/src/.gitignore
+++ b/common/lib/xmodule/xmodule/js/src/.gitignore
@@ -1,2 +1,2 @@
-*.js
+
diff --git a/common/lib/xmodule/xmodule/js/src/conditional/display.coffee b/common/lib/xmodule/xmodule/js/src/conditional/display.coffee
index 33dcb29079..af59a3a465 100644
--- a/common/lib/xmodule/xmodule/js/src/conditional/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/conditional/display.coffee
@@ -1,26 +1,45 @@
class @Conditional
- constructor: (element) ->
+ constructor: (element, callerElId) ->
@el = $(element).find('.conditional-wrapper')
- @id = @el.data('problem-id')
- @element_id = @el.attr('id')
- @url = @el.data('url')
- @render()
- $: (selector) ->
- $(selector, @el)
+ @callerElId = callerElId
- 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)
+ if @el.data('passed') is true
+ return
+ else if @el.data('passed') is false
+ @passed = false
else
- $.postWithPrefix "#{@url}/conditional_get", (response) =>
- @el.html(response.html)
- XModule.loadModules(@el)
+ @passed = null
+ if callerElId isnt undefined and @passed isnt null
+ dependencies = @el.data('depends')
+ if (typeof dependencies is 'string') and (dependencies.length > 0) and (dependencies.indexOf(callerElId) is -1)
+ return
+
+ @url = @el.data('url')
+ @render(element)
+
+ render: (element) ->
+ $.postWithPrefix "#{@url}/conditional_get", (response) =>
+ if (((response.passed is true) && (@passed is false)) || (@passed is null))
+ @el.data 'passed', response.passed
+
+ @el.html ''
+ @el.append(i) for i in response.html
+
+ parentEl = $(element).parent()
+ parentId = parentEl.attr 'id'
+
+ if response.message is false
+ if parentId.indexOf('vert') is 0
+ parentEl.hide()
+ else
+ $(element).hide()
+ else
+ if parentId.indexOf('vert') is 0
+ parentEl.show()
+ else
+ $(element).show()
+
+ XModule.loadModules @el
diff --git a/common/lib/xmodule/xmodule/js/src/poll/display.coffee b/common/lib/xmodule/xmodule/js/src/poll/display.coffee
deleted file mode 100644
index f72ac8d319..0000000000
--- a/common/lib/xmodule/xmodule/js/src/poll/display.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-class @PollModule
- constructor: (element) ->
- @el = element
- @ajaxUrl = @$('.container').data('url')
- @$('.upvote').on('click', () => $.postWithPrefix(@url('upvote'), @handleVote))
- @$('.downvote').on('click', () => $.postWithPrefix(@url('downvote'), @handleVote))
-
- $: (selector) -> $(selector, @el)
-
- url: (target) -> "#{@ajaxUrl}/#{target}"
-
- handleVote: (response) =>
- @$('.container').replaceWith(response.results)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/js/src/poll/logme.js b/common/lib/xmodule/xmodule/js/src/poll/logme.js
new file mode 100644
index 0000000000..c045757044
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/poll/logme.js
@@ -0,0 +1,54 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+(function (requirejs, require, define) {
+
+define('logme', [], function () {
+ var debugMode;
+
+ // debugMode can be one of the following:
+ //
+ // true - All messages passed to logme will be written to the internal
+ // browser console.
+ // false - Suppress all output to the internal browser console.
+ //
+ // Obviously, if anywhere there is a direct console.log() call, we can't do
+ // anything about it. That's why use logme() - it will allow to turn off
+ // the output of debug information with a single change to a variable.
+ debugMode = true;
+
+ return logme;
+
+ /*
+ * function: logme
+ *
+ * A helper function that provides logging facilities. We don't want
+ * to call console.log() directly, because sometimes it is not supported
+ * by the browser. Also when everything is routed through this function.
+ * the logging output can be easily turned off.
+ *
+ * logme() supports multiple parameters. Each parameter will be passed to
+ * console.log() function separately.
+ *
+ */
+ function logme() {
+ var i;
+
+ if (
+ (typeof debugMode === 'undefined') ||
+ (debugMode !== true) ||
+ (typeof window.console === 'undefined')
+ ) {
+ return;
+ }
+
+ for (i = 0; i < arguments.length; i++) {
+ window.console.log(arguments[i]);
+ }
+ } // End-of: function logme
+});
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/common/lib/xmodule/xmodule/js/src/poll/poll.js b/common/lib/xmodule/xmodule/js/src/poll/poll.js
new file mode 100644
index 0000000000..a2ccbc7c03
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/poll/poll.js
@@ -0,0 +1,5 @@
+window.Poll = function (el) {
+ RequireJS.require(['PollMain'], function (PollMain) {
+ new PollMain(el);
+ });
+};
diff --git a/common/lib/xmodule/xmodule/js/src/poll/poll_main.js b/common/lib/xmodule/xmodule/js/src/poll/poll_main.js
new file mode 100644
index 0000000000..a5cec07095
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/poll/poll_main.js
@@ -0,0 +1,255 @@
+(function (requirejs, require, define) {
+define('PollMain', ['logme'], function (logme) {
+
+PollMain.prototype = {
+
+'showAnswerGraph': function (poll_answers, total) {
+ var _this, totalValue;
+
+ totalValue = parseFloat(total);
+ if (isFinite(totalValue) === false) {
+ return;
+ }
+
+ _this = this;
+
+ $.each(poll_answers, function (index, value) {
+ var numValue, percentValue;
+
+ numValue = parseFloat(value);
+ if (isFinite(numValue) === false) {
+ return;
+ }
+
+ percentValue = (numValue / totalValue) * 100.0;
+
+ _this.answersObj[index].statsEl.show();
+ _this.answersObj[index].numberEl.html('' + value + ' (' + percentValue.toFixed(1) + '%)');
+ _this.answersObj[index].percentEl.css({
+ 'width': '' + percentValue.toFixed(1) + '%'
+ });
+ });
+},
+
+'submitAnswer': function (answer, answerObj) {
+ var _this;
+
+ // Make sure that the user can answer a question only once.
+ if (this.questionAnswered === true) {
+ return;
+ }
+ this.questionAnswered = true;
+
+ _this = this;
+
+ answerObj.buttonEl.addClass('answered');
+
+ // Send the data to the server as an AJAX request. Attach a callback that will
+ // be fired on server's response.
+ $.postWithPrefix(
+ _this.ajax_url + '/' + answer, {},
+ function (response) {
+ _this.showAnswerGraph(response.poll_answers, response.total);
+
+ if (_this.wrapperSectionEl !== null) {
+ $(_this.wrapperSectionEl).find('.xmodule_ConditionalModule').each(function (index, value) {
+ new window.Conditional(value, _this.id.replace(/^poll_/, ''));
+ });
+ }
+ }
+ );
+}, // End-of: 'submitAnswer': function (answer, answerEl) {
+
+'postInit': function () {
+ var _this;
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ if (
+ (this.jsonConfig.poll_answer.length > 0) &&
+ (this.jsonConfig.answers.hasOwnProperty(this.jsonConfig.poll_answer) === false)
+ ) {
+ this.questionEl.append(
+ '
Error!
' +
+ '
XML data format changed. List of answers was modified, but poll data was not updated.
'
+ );
+
+ return;
+ }
+
+ // Get the DOM id of the question.
+ this.id = this.questionEl.attr('id');
+
+ // Get the URL to which we will post the users answer to the question.
+ this.ajax_url = this.questionEl.data('ajax-url');
+
+ this.questionHtmlMarkup = $('').html(this.jsonConfig.question).text();
+ this.questionEl.append(this.questionHtmlMarkup);
+
+ // When the user selects and answer, we will set this flag to true.
+ this.questionAnswered = false;
+
+ this.answersObj = {};
+ this.shortVersion = true;
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ if (value.length >= 18) {
+ _this.shortVersion = false;
+ }
+ });
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ var answer;
+
+ answer = {};
+
+ _this.answersObj[index] = answer;
+
+ answer.el = $('');
+
+ answer.questionEl = $('');
+ answer.buttonEl = $('');
+ answer.textEl = $('');
+ answer.questionEl.append(answer.buttonEl);
+ answer.questionEl.append(answer.textEl);
+
+ answer.el.append(answer.questionEl);
+
+ answer.statsEl = $('');
+ answer.barEl = $('');
+ answer.percentEl = $('');
+ answer.barEl.append(answer.percentEl);
+ answer.numberEl = $('');
+ answer.statsEl.append(answer.barEl);
+ answer.statsEl.append(answer.numberEl);
+
+ answer.statsEl.hide();
+
+ answer.el.append(answer.statsEl);
+
+ answer.textEl.html(value);
+
+ if (_this.shortVersion === true) {
+ $.each(answer, function (index, value) {
+ if (value instanceof jQuery) {
+ value.addClass('short');
+ }
+ });
+ }
+
+ answer.el.appendTo(_this.questionEl);
+
+ answer.textEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ answer.buttonEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ if (index === _this.jsonConfig.poll_answer) {
+ answer.buttonEl.addClass('answered');
+ _this.questionAnswered = true;
+ }
+ });
+
+ this.graphAnswerEl = $('');
+ this.graphAnswerEl.hide();
+ this.graphAnswerEl.appendTo(this.questionEl);
+
+ // If it turns out that the user already answered the question, show the answers graph.
+ if (this.questionAnswered === true) {
+ this.showAnswerGraph(this.jsonConfig.poll_answers, this.jsonConfig.total);
+ }
+} // End-of: 'postInit': function () {
+}; // End-of: PollMain.prototype = {
+
+return PollMain;
+
+function PollMain(el) {
+ var _this;
+
+ this.questionEl = $(el).find('.poll_question');
+ if (this.questionEl.length !== 1) {
+ // We require one question DOM element.
+ logme('ERROR: PollMain constructor requires one question DOM element.');
+
+ return;
+ }
+
+ // Just a safety precussion. If we run this code more than once, multiple 'click' callback handlers will be
+ // attached to the same DOM elements. We don't want this to happen.
+ if (this.questionEl.attr('poll_main_processed') === 'true') {
+ logme(
+ 'ERROR: PolMain JS constructor was called on a DOM element that has already been processed once.'
+ );
+
+ return;
+ }
+
+ // This element was not processed earlier.
+ // Make sure that next time we will not process this element a second time.
+ this.questionEl.attr('poll_main_processed', 'true');
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ // DOM element which contains the current poll along with any conditionals. By default we assume that such
+ // element is not present. We will try to find it.
+ this.wrapperSectionEl = null;
+
+ (function (tempEl, c1) {
+ while (tempEl.tagName.toLowerCase() !== 'body') {
+ tempEl = $(tempEl).parent()[0];
+ c1 += 1;
+
+ if (
+ (tempEl.tagName.toLowerCase() === 'section') &&
+ ($(tempEl).hasClass('xmodule_WrapperModule') === true)
+ ) {
+ _this.wrapperSectionEl = tempEl;
+
+ break;
+ } else if (c1 > 50) {
+ // In case something breaks, and we enter an endless loop, a sane
+ // limit for loop iterations.
+
+ break;
+ }
+ }
+ }($(el)[0], 0));
+
+ try {
+ this.jsonConfig = JSON.parse(this.questionEl.children('.poll_question_div').html());
+
+ $.postWithPrefix(
+ '' + this.questionEl.data('ajax-url') + '/' + 'get_state', {},
+ function (response) {
+ _this.jsonConfig.poll_answer = response.poll_answer;
+ _this.jsonConfig.total = response.total;
+
+ $.each(response.poll_answers, function (index, value) {
+ _this.jsonConfig.poll_answers[index] = value;
+ });
+
+ _this.questionEl.children('.poll_question_div').html(JSON.stringify(_this.jsonConfig));
+ _this.postInit();
+ }
+ );
+
+ return;
+ } catch (err) {
+ logme(
+ 'ERROR: Invalid JSON config for poll ID "' + this.id + '".',
+ 'Error messsage: "' + err.message + '".'
+ );
+
+ return;
+ }
+} // End-of: function PollMain(el) {
+
+}); // End-of: define('PollMain', ['logme'], function (logme) {
+
+// End-of: (function (requirejs, require, define) {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee b/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
new file mode 100644
index 0000000000..a13c5a8bc7
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
@@ -0,0 +1,10 @@
+class @WrapperDescriptor extends XModule.Descriptor
+ constructor: (@element) ->
+ console.log 'WrapperDescriptor'
+ @$items = $(@element).find(".vert-mod")
+ @$items.sortable(
+ update: (event, ui) => @update()
+ )
+
+ save: ->
+ children: $('.vert-mod li', @element).map((idx, el) -> $(el).data('id')).toArray()
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 5675ce1037..aec1706aad 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -74,7 +74,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check)
# tags that really need unique names--they store (or should store) state.
- need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'timelimit')
+ need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter',
+ 'videosequence', 'poll_question', 'timelimit')
attr = xml_data.attrib
tag = xml_data.tag
diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py
index eb5bef9e6d..a79781ef86 100644
--- a/common/lib/xmodule/xmodule/poll_module.py
+++ b/common/lib/xmodule/xmodule/poll_module.py
@@ -1,54 +1,196 @@
+"""Poll module is ungraded xmodule used by students to
+to do set of polls.
+
+On the client side we show:
+If student does not yet anwered - Question with set of choices.
+If student have answered - Question with statistics for each answers.
+
+Student can't change his answer.
+"""
+
+import cgi
import json
import logging
+from copy import deepcopy
+from collections import OrderedDict
from lxml import etree
-from pkg_resources import resource_string, resource_listdir
+from pkg_resources import resource_string
from xmodule.x_module import XModule
-from xmodule.raw_module import RawDescriptor
-from xblock.core import Integer, Scope, Boolean
+from xmodule.stringify import stringify_children
+from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.xml_module import XmlDescriptor
+from xblock.core import Scope, String, Object, Boolean, List
log = logging.getLogger(__name__)
class PollModule(XModule):
+ """Poll Module"""
+ js = {
+ 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
+ 'js': [resource_string(__name__, 'js/src/poll/logme.js'),
+ resource_string(__name__, 'js/src/poll/poll.js'),
+ resource_string(__name__, 'js/src/poll/poll_main.js')]
+ }
+ css = {'scss': [resource_string(__name__, 'css/poll/display.scss')]}
+ js_module_name = "Poll"
- js = {'coffee': [resource_string(__name__, 'js/src/poll/display.coffee')]}
- js_module_name = "PollModule"
+ # Name of poll to use in links to this poll
+ display_name = String(help="Display name for this module", scope=Scope.settings)
- upvotes = Integer(help="Number of upvotes this poll has recieved", scope=Scope.content, default=0)
- downvotes = Integer(help="Number of downvotes this poll has recieved", scope=Scope.content, default=0)
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
+ poll_answer = String(help="Student answer", scope=Scope.student_state, default='')
+ poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
+
+ answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
+ question = String(help="Poll question", scope=Scope.content, default='')
def handle_ajax(self, dispatch, get):
- '''
- Handle ajax calls to this video.
- TODO (vshnayder): This is not being called right now, so the position
- is not being saved.
- '''
- if self.voted:
- return json.dumps({'error': 'Already Voted!'})
- elif dispatch == 'upvote':
- self.upvotes += 1
- self.voted = True
- return json.dumps({'results': self.get_html()})
- elif dispatch == 'downvote':
- self.downvotes += 1
- self.voted = True
- return json.dumps({'results': self.get_html()})
+ """Ajax handler.
- return json.dumps({'error': 'Unknown Command!'})
+ Args:
+ dispatch: string request slug
+ get: dict request get parameters
+
+ Returns:
+ json string
+ """
+ if dispatch in self.poll_answers and not self.voted:
+ # FIXME: fix this, when xblock will support mutable types.
+ # Now we use this hack.
+ temp_poll_answers = self.poll_answers
+ temp_poll_answers[dispatch] += 1
+ self.poll_answers = temp_poll_answers
+
+ self.voted = True
+ self.poll_answer = dispatch
+ return json.dumps({'poll_answers': self.poll_answers,
+ 'total': sum(self.poll_answers.values()),
+ 'callback': {'objectName': 'Conditional'}
+ })
+ elif dispatch == 'get_state':
+ return json.dumps({'poll_answer': self.poll_answer,
+ 'poll_answers': self.poll_answers,
+ 'total': sum(self.poll_answers.values())
+ })
+ else: # return error message
+ return json.dumps({'error': 'Unknown Command!'})
def get_html(self):
- return self.system.render_template('poll.html', {
- 'upvotes': self.upvotes,
- 'downvotes': self.downvotes,
- 'voted': self.voted,
- 'ajax_url': self.system.ajax_url,
- })
+ """Renders parameters to template."""
+ params = {
+ 'element_id': self.location.html_id(),
+ 'element_class': self.location.category,
+ 'ajax_url': self.system.ajax_url,
+ 'configuration_json': self.dump_poll(),
+ }
+ self.content = self.system.render_template('poll.html', params)
+ return self.content
+
+ def dump_poll(self):
+ """Dump poll information.
+
+ Returns:
+ string - Serialize json.
+ """
+ # FIXME: hack for resolving caching `default={}` during definition
+ # poll_answers field
+ if self.poll_answers is None:
+ self.poll_answers = {}
+
+ answers_to_json = OrderedDict()
+
+ # FIXME: fix this, when xblock support mutable types.
+ # Now we use this hack.
+ temp_poll_answers = self.poll_answers
+
+ # Fill self.poll_answers, prepare data for template context.
+ for answer in self.answers:
+ # Set default count for answer = 0.
+ if answer['id'] not in temp_poll_answers:
+ temp_poll_answers[answer['id']] = 0
+ answers_to_json[answer['id']] = cgi.escape(answer['text'])
+ self.poll_answers = temp_poll_answers
+
+ return json.dumps({'answers': answers_to_json,
+ 'question': cgi.escape(self.question),
+ # to show answered poll after reload:
+ 'poll_answer': self.poll_answer,
+ 'poll_answers': self.poll_answers if self.voted else {},
+ 'total': sum(self.poll_answers.values()) if self.voted else 0})
-class PollDescriptor(RawDescriptor):
+class PollDescriptor(MakoModuleDescriptor, XmlDescriptor):
+ _tag_name = 'poll_question'
+ _child_tag_name = 'answer'
+
module_class = PollModule
+ template_dir_name = 'poll'
stores_state = True
- template_dir_name = "poll"
\ No newline at end of file
+
+ answers = List(help="Poll answers", scope=Scope.content, default=[])
+ question = String(help="Poll question", scope=Scope.content, default='')
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+ id = String(help="ID attribute for this module", scope=Scope.settings)
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ """Pull out the data into dictionary.
+
+ Args:
+ xml_object: xml from file.
+ system: `system` object.
+
+ Returns:
+ (definition, children) - tuple
+ definition - dict:
+ {
+ 'answers': ,
+ 'question':
+ }
+ """
+ # Check for presense of required tags in xml.
+ if len(xml_object.xpath(cls._child_tag_name)) == 0:
+ raise ValueError("Poll_question definition must include \
+ at least one 'answer' tag")
+
+ xml_object_copy = deepcopy(xml_object)
+ answers = []
+ for element_answer in xml_object_copy.findall(cls._child_tag_name):
+ answer_id = element_answer.get('id', None)
+ if answer_id:
+ answers.append({
+ 'id': answer_id,
+ 'text': stringify_children(element_answer)
+ })
+ xml_object_copy.remove(element_answer)
+
+ definition = {
+ 'answers': answers,
+ 'question': stringify_children(xml_object_copy)
+ }
+ children = []
+
+ return (definition, children)
+
+ def definition_to_xml(self, resource_fs):
+ """Return an xml element representing to this definition."""
+ poll_str = '<{tag_name}>{text}{tag_name}>'.format(
+ tag_name=self._tag_name, text=self.question)
+ xml_object = etree.fromstring(poll_str)
+ xml_object.set('display_name', self.display_name)
+ xml_object.set('id', self.id)
+
+ def add_child(xml_obj, answer):
+ child_str = '<{tag_name} id="{id}">{text}{tag_name}>'.format(
+ tag_name=self._child_tag_name, id=answer['id'],
+ text=answer['text'])
+ child_node = etree.fromstring(child_str)
+ xml_object.append(child_node)
+
+ for answer in self.answers:
+ add_child(xml_object, answer)
+
+ return xml_object
diff --git a/common/lib/xmodule/xmodule/stringify.py b/common/lib/xmodule/xmodule/stringify.py
index 5a640e91b1..35587d3b09 100644
--- a/common/lib/xmodule/xmodule/stringify.py
+++ b/common/lib/xmodule/xmodule/stringify.py
@@ -1,4 +1,5 @@
-from itertools import chain
+# -*- coding: utf-8 -*-
+
from lxml import etree
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index 815550f5ff..5244856889 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -28,7 +28,6 @@ def strip_filenames(descriptor):
strip_filenames(d)
-
class RoundTripTestCase(unittest.TestCase):
''' Check that our test courses roundtrip properly.
Same course imported , than exported, then imported again.
@@ -91,7 +90,6 @@ class RoundTripTestCase(unittest.TestCase):
self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location])
-
def setUp(self):
self.maxDiff = None
@@ -104,6 +102,9 @@ class RoundTripTestCase(unittest.TestCase):
def test_full_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "full")
+ def test_conditional_and_poll_roundtrip(self):
+ self.check_export_roundtrip(DATA_DIR, "conditional_and_poll")
+
def test_selfassessment_roundtrip(self):
#Test selfassessment xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "self_assessment")
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index c4547241e0..8f32f9b957 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
from path import path
import unittest
from fs.memoryfs import MemoryFS
@@ -76,7 +78,6 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(descriptor.__class__.__name__,
'ErrorDescriptor')
-
def test_unique_url_names(self):
'''Check that each error gets its very own url_name'''
bad_xml = ''''''
@@ -88,7 +89,6 @@ class ImportTestCase(BaseCourseTestCase):
self.assertNotEqual(descriptor1.location, descriptor2.location)
-
def test_reimport(self):
'''Make sure an already-exported error xml tag loads properly'''
@@ -230,7 +230,6 @@ class ImportTestCase(BaseCourseTestCase):
check_for_key('graceperiod', course)
-
def test_policy_loading(self):
"""Make sure that when two courses share content with the same
org and course names, policy applies to the right one."""
@@ -254,7 +253,6 @@ class ImportTestCase(BaseCourseTestCase):
# appropriate attribute maps -- 'graded' should be True, not 'true'
self.assertEqual(toy.lms.graded, True)
-
def test_definition_loading(self):
"""When two courses share the same org and course name and
both have a module with the same url_name, the definitions shouldn't clash.
@@ -274,7 +272,6 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8")
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9")
-
def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly"""
@@ -331,6 +328,22 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(len(video.url_name), len('video_') + 12)
+ def test_poll_xmodule(self):
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=['conditional_and_poll'])
+
+ course = modulestore.get_courses()[0]
+ chapters = course.get_children()
+ ch1 = chapters[0]
+ sections = ch1.get_children()
+
+ self.assertEqual(len(sections), 1)
+
+ location = course.location
+ location = Location(location.tag, location.org, location.course,
+ 'sequential', 'Problem_Demos')
+ module = modulestore.get_instance(course.id, location)
+ self.assertEqual(len(module.children), 2)
+
def test_error_on_import(self):
'''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorModule'''
diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py
new file mode 100644
index 0000000000..6b9a636590
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_logic.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+import json
+import unittest
+
+from xmodule.poll_module import PollModule
+from xmodule.conditional_module import ConditionalModule
+
+
+class LogicTest(unittest.TestCase):
+ """Base class for testing xmodule logic."""
+ xmodule_class = None
+ raw_model_data = {}
+
+ def setUp(self):
+ self.system = None
+ self.location = None
+ self.descriptor = None
+
+ self.xmodule = self.xmodule_class(self.system, self.location,
+ self.descriptor, self.raw_model_data)
+
+ def ajax_request(self, dispatch, get):
+ return json.loads(self.xmodule.handle_ajax(dispatch, get))
+
+
+class PollModuleTest(LogicTest):
+ xmodule_class = PollModule
+ raw_model_data = {
+ 'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0},
+ 'voted': False,
+ 'poll_answer': ''
+ }
+
+ def test_bad_ajax_request(self):
+ response = self.ajax_request('bad_answer', {})
+ self.assertDictEqual(response, {'error': 'Unknown Command!'})
+
+ def test_good_ajax_request(self):
+ response = self.ajax_request('No', {})
+
+ poll_answers = response['poll_answers']
+ total = response['total']
+ callback = response['callback']
+
+ self.assertDictEqual(poll_answers, {'Yes': 1, 'Dont_know': 0, 'No': 1})
+ self.assertEqual(total, 2)
+ self.assertDictEqual(callback, {'objectName': 'Conditional'})
+ self.assertEqual(self.xmodule.poll_answer, 'No')
+
+
+class ConditionalModuleTest(LogicTest):
+ xmodule_class = ConditionalModule
+ raw_model_data = {
+ 'contents': 'Some content'
+ }
+
+ def test_ajax_request(self):
+ # Mock is_condition_satisfied
+ self.xmodule.is_condition_satisfied = lambda: True
+
+ response = self.ajax_request('No', {})
+ html = response['html']
+
+ self.assertEqual(html, 'Some content')
diff --git a/common/lib/xmodule/xmodule/wrapper_module.py b/common/lib/xmodule/xmodule/wrapper_module.py
new file mode 100644
index 0000000000..d675f102bf
--- /dev/null
+++ b/common/lib/xmodule/xmodule/wrapper_module.py
@@ -0,0 +1,26 @@
+# Same as vertical,
+# But w/o css delimiters between children
+
+from xmodule.vertical_module import VerticalModule, VerticalDescriptor
+from pkg_resources import resource_string
+
+# HACK: This shouldn't be hard-coded to two types
+# OBSOLETE: This obsoletes 'type'
+class_priority = ['video', 'problem']
+
+
+class WrapperModule(VerticalModule):
+ ''' Layout module for laying out submodules vertically w/o css delimiters'''
+
+ has_children = True
+ css = {'scss': [resource_string(__name__, 'css/wrapper/display.scss')]}
+
+
+class WrapperDescriptor(VerticalDescriptor):
+ module_class = WrapperModule
+
+ has_children = True
+
+ js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
+ js_module_name = "VerticalDescriptor"
+
diff --git a/common/test/data/conditional_and_poll/README b/common/test/data/conditional_and_poll/README
new file mode 100644
index 0000000000..fc95a7c0c9
--- /dev/null
+++ b/common/test/data/conditional_and_poll/README
@@ -0,0 +1,50 @@
+Any place that says "YEAR_SEMESTER" needs to be replaced with something
+in the form "2013_Spring". Take note of this name exactly, you'll need to
+use it everywhere, precisely - capitalization is very important.
+
+See https://github.com/MITx/mitx/blob/master/doc/xml-format.md for more on all this.
+-----------------------
+
+about/: Files that live here will be visible OUTSIDE OF COURSEWARE.
+ YEAR_SEMESTER/
+ end_date.html: Specifies in plain-text the end date of the course
+ overview.html: Text of the overview of the course
+ short_description.html: 10-15 words about the course
+ prerequisites.html: Any prerequisites for the course, or None if there are none.
+
+course/
+ YEAR_SEMESTER.xml: This is your top-level xml page that points at chapters.
+ Can just be for now.
+
+course.xml: This top level file points at a file in roots/. See creating_course.xml.
+
+creating_course.xml: Explains how to create course.xml
+
+info/: Files that live here will be visible on the COURSE LANDING PAGE
+ (Course Info) WITHIN THE COURSEWARE.
+ YEAR_SEMESTER/
+ handouts.html: A list of handouts, or an empty file if there are none
+ (if this file doesn't exist, it displays an error)
+ updates.html: Course updates.
+
+policies/
+ YEAR_SEMESTER/
+ policy.json: See https://github.com/MITx/mitx/blob/master/doc/xml-format.md
+ for more on the fields specified by this file.
+ grading_policy.json: Optional -- you don't need it to get a course off the
+ ground but will eventually. For more info see
+ https://github.com/MITx/mitx/blob/master/doc/course_grading.md
+
+roots/
+ YEAR_SEMESTER.xml: Looks something like
+
+ where ORG in {"MITx", "HarvardX", "BerkeleyX"}
+
+static/
+ See README.
+
+ images/
+ course_image.jpg: You MUST have an image named this to be the background
+ banner image on edx.org
+
+-----------------------
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/README.md b/common/test/data/conditional_and_poll/README.md
new file mode 100644
index 0000000000..7dbfa46a26
--- /dev/null
+++ b/common/test/data/conditional_and_poll/README.md
@@ -0,0 +1,2 @@
+content-harvard-justicex
+========================
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/about/2013_Spring/overview.html b/common/test/data/conditional_and_poll/about/2013_Spring/overview.html
new file mode 100644
index 0000000000..9c49899948
--- /dev/null
+++ b/common/test/data/conditional_and_poll/about/2013_Spring/overview.html
@@ -0,0 +1,79 @@
+
+
+
+
About ER22x
+
+
Justice is a critical analysis of classical and contemporary theories of justice, including discussion of present-day applications. Topics include affirmative action, income distribution, same-sex marriage, the role of markets, debates about rights (human rights and property rights), arguments for and against equality, dilemmas of loyalty in public and private life. The course invites students to subject their own views on these controversies to critical examination.
+
+
The principle readings for the course are texts by Aristotle, John Locke, Immanuel Kant, John Stuart Mill, and John Rawls. Other assigned readings include writings by contemporary philosophers, court cases, and articles about political controversies that raise philosophical questions.
+
+
+
+
+
+
+
+
Course instructor
+
+
+
+
+
Michael J. Sandel
+
Michael J. Sandel is the Anne T. and Robert M. Bass Professor of Government at Harvard University, where he teaches political philosophy. His course "Justice" has enrolled more than 15,000 Harvard students. Sandel's writings have been published in 21 languages. His books include What Money Can't Buy: The Moral Limits of Markets (2012); Justice: What's the Right Thing to Do? (2009); The Case against Perfection: Ethics in the Age of Genetic Engineering (2007); Public Philosophy: Essays on Morality in Politics (2005); Democracy's Discontent (1996); and Liberalism and the Limits of Justice(1982; 2nd ed., 1998).
+
+
+
+
+
+
+
Frequently Asked Questions
+
+
How much does it cost to take the course?
+
Nothing! The course is free.
+
+
+
+
Does the course have any prerequisites?
+
No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.
+
+
+
+
Do I need any other materials to take the course?
+
No. As long as you’ve got a computer to access the website, you are ready to take the course.
+
+
+
+
Is there a textbook for the course?
+
All of the course readings that are in the public domain are freely available online, at links provided on the course website. The course can be taken using these free resources alone. For those who wish to purchase a printed version of the assigned readings, an edited volume entitled, Justice: A Reader (ed., Michael Sandel) is available in paperback from Oxford University Press (in bookstores and from online booksellers). Those who would like supplementary readings on the themes of the lectures can find them in Michael Sandel's book Justice: What's the Right Thing to Do?, which is available in various languages throughout the world. This book is not required, and the course can be taken using the free online resources alone.
+
+
+
+
Do I need to watch the lectures at a specific time?
+
No. You can watch the lectures at your leisure.
+
+
+
+
Will I be able to participate in class discussions?
+
Yes, in several ways:
+
+
+
Each lecture invites you to respond to a poll question related to the themes of the lecture. If you respond to the question, you will be presented with a challenge to the opinion you have expressed, and invited to reply to the challenge. You can also, if you wish, comment on the opinions and responses posted by other students in the course, continuing the discussion.
+
+
In addition to the poll question, each class contains a discussion prompt that invites you to offer your view on a controversial question related to the lecture. If you wish, you can respond to this question, and then see what other students have to say about the argument you present. You can also comment on the opinions posted by other students. One aim of the course is to promote reasoned public dialogue about hard moral and political questions.
+
+
Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.
+
+
+
+
+
Will certificates be awarded?
+
Yes. Online learners who achieve a passing grade in a course can earn a certificate of mastery. These certificates will indicate you have successfully completed the course, but will not include a specific grade. Certificates will be issued by edX under the name of HarvardX, designating the institution from which the course originated.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/about/2013_Spring/prerequisites.html b/common/test/data/conditional_and_poll/about/2013_Spring/prerequisites.html
new file mode 100644
index 0000000000..b0047fa49f
--- /dev/null
+++ b/common/test/data/conditional_and_poll/about/2013_Spring/prerequisites.html
@@ -0,0 +1 @@
+None
diff --git a/common/test/data/conditional_and_poll/about/2013_Spring/short_description.html b/common/test/data/conditional_and_poll/about/2013_Spring/short_description.html
new file mode 100644
index 0000000000..208880c842
--- /dev/null
+++ b/common/test/data/conditional_and_poll/about/2013_Spring/short_description.html
@@ -0,0 +1 @@
+JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/about/2013_Spring/video.html b/common/test/data/conditional_and_poll/about/2013_Spring/video.html
new file mode 100644
index 0000000000..0cf427b16c
--- /dev/null
+++ b/common/test/data/conditional_and_poll/about/2013_Spring/video.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/chapter/Staff.xml b/common/test/data/conditional_and_poll/chapter/Staff.xml
new file mode 100644
index 0000000000..e1d5216f6d
--- /dev/null
+++ b/common/test/data/conditional_and_poll/chapter/Staff.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/common/test/data/conditional_and_poll/course.xml b/common/test/data/conditional_and_poll/course.xml
new file mode 120000
index 0000000000..f4f5c17b87
--- /dev/null
+++ b/common/test/data/conditional_and_poll/course.xml
@@ -0,0 +1 @@
+roots/2013_Spring.xml
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/course/2013_Spring.xml b/common/test/data/conditional_and_poll/course/2013_Spring.xml
new file mode 100644
index 0000000000..cb6e7c1217
--- /dev/null
+++ b/common/test/data/conditional_and_poll/course/2013_Spring.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/common/test/data/conditional_and_poll/creating_course.xml b/common/test/data/conditional_and_poll/creating_course.xml
new file mode 100644
index 0000000000..4c90f1c2ec
--- /dev/null
+++ b/common/test/data/conditional_and_poll/creating_course.xml
@@ -0,0 +1,8 @@
+
diff --git a/common/test/data/conditional_and_poll/info/2013_Spring/handouts.html b/common/test/data/conditional_and_poll/info/2013_Spring/handouts.html
new file mode 100644
index 0000000000..35f2c89474
--- /dev/null
+++ b/common/test/data/conditional_and_poll/info/2013_Spring/handouts.html
@@ -0,0 +1,3 @@
+
+
A list of course handouts, or an empty file if there are none.
Suppose four shipwrecked sailors are stranded at sea in a lifeboat, without
+ food or water. Would it be wrong for three of them to kill and eat the cabin
+ boy, in order to save their own lives?
+ Yes
+ No
+ Don't know
+
+
+
What's the Right Thing to Do?
+
Suppose four shipwrecked sailors are stranded at sea in a lifeboat, without
+ food or water. Would it be wrong for three of them to kill and eat the cabin
+ boy, in order to save their own lives?
+ Yes
+ No
+ Don't know
+
+
+
+
+
+ Condition: first_poll - Yes
+
+ In first condition.
+
+
+
+
diff --git a/common/test/data/conditional_and_poll/static/README b/common/test/data/conditional_and_poll/static/README
new file mode 100644
index 0000000000..e22f378b5e
--- /dev/null
+++ b/common/test/data/conditional_and_poll/static/README
@@ -0,0 +1,5 @@
+Images, handouts, and other statically-served content should go ONLY
+in this directory.
+
+Images for the front page should go in static/images. The frontpage
+banner MUST be named course_image.jpg
\ No newline at end of file
diff --git a/common/test/data/conditional_and_poll/static/images/course_image.jpg b/common/test/data/conditional_and_poll/static/images/course_image.jpg
new file mode 100644
index 0000000000..b6a64b9396
Binary files /dev/null and b/common/test/data/conditional_and_poll/static/images/course_image.jpg differ
diff --git a/common/test/data/conditional_and_poll/static/images/professor-sandel.jpg b/common/test/data/conditional_and_poll/static/images/professor-sandel.jpg
new file mode 100644
index 0000000000..41bde60165
Binary files /dev/null and b/common/test/data/conditional_and_poll/static/images/professor-sandel.jpg differ
diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py
index d13bfe9bba..e183b6ca93 100644
--- a/lms/djangoapps/courseware/model_data.py
+++ b/lms/djangoapps/courseware/model_data.py
@@ -75,7 +75,7 @@ class ModelDataCache(object):
if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth
- for child in descriptor.get_children():
+ for child in descriptor.get_children() + descriptor.get_required_module_descriptors():
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
return descriptors
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index c4d3d8e772..cf60b74fd8 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -35,7 +35,7 @@ SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
# Enable Berkeley forums
-MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
+MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
# IMPORTANT: With this enabled, the server must always be behind a proxy that
# strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise,
diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss
index 038903b756..ad084423db 100644
--- a/lms/static/sass/course/courseware/_courseware.scss
+++ b/lms/static/sass/course/courseware/_courseware.scss
@@ -119,6 +119,10 @@ div.course-wrapper {
}
}
+ section.xmodule_WrapperModule ol.vert-mod > li {
+ border-bottom: none;
+ }
+
section.tutorials {
h2 {
margin-bottom: lh();
@@ -219,7 +223,7 @@ div.course-wrapper {
.xmodule_VideoModule {
margin-bottom: 30px;
-
+
}
textarea.short-form-response {
@@ -237,7 +241,7 @@ section.self-assessment {
margin-top: 5px;
margin-bottom: 5px;
}
-
+
div {
margin-top: 5px;
margin-bottom: 5px;
diff --git a/lms/templates/conditional_ajax.html b/lms/templates/conditional_ajax.html
index 0a5887be04..61f1095259 100644
--- a/lms/templates/conditional_ajax.html
+++ b/lms/templates/conditional_ajax.html
@@ -1 +1,8 @@
-
+
+
\ No newline at end of file
diff --git a/rakefile b/rakefile
index a90c55a37a..6d16db8a4c 100644
--- a/rakefile
+++ b/rakefile
@@ -183,6 +183,12 @@ end
TEST_TASK_DIRS = []
+task :fastlms do
+ # this is >2 times faster that rake [lms], and does not need web, good for local dev
+ django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
+ sh("#{django_admin} runserver --traceback --settings=lms.envs.dev --pythonpath=.")
+end
+
[:lms, :cms].each do |system|
report_dir = report_dir_path(system)