poll and conditional finished
This commit is contained in:
@@ -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'<span class="math">5[Ni(NH<sub>3</sub>)<sub>4</sub>]<sup>2+</sup>+<sup>5</sup>⁄<sub>2</sub>SO<sub>4</sub><sup>2-</sup></span>'
|
||||
log(out + ' ------- ' + correct, 'html')
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
|
||||
<conditional sources="i4x://.../problem_1; i4x://.../problem_2" completed="True">
|
||||
<show sources="i4x://.../test_6; i4x://.../Avi_resources"/>
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
|
||||
<conditional condition="require_attempted" required="tag/url_name1&tag/url_name2">
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
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: <tag attribute in xml>
|
||||
# value: <name of module attribute>
|
||||
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
|
||||
|
||||
226
common/lib/xmodule/xmodule/css/poll/display.scss
Normal file
226
common/lib/xmodule/xmodule/css/poll/display.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
0
common/lib/xmodule/xmodule/css/wrapper/display.scss
Normal file
0
common/lib/xmodule/xmodule/css/wrapper/display.scss
Normal file
2
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
2
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
*.js
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
54
common/lib/xmodule/xmodule/js/src/poll/logme.js
Normal file
54
common/lib/xmodule/xmodule/js/src/poll/logme.js
Normal file
@@ -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)
|
||||
5
common/lib/xmodule/xmodule/js/src/poll/poll.js
Normal file
5
common/lib/xmodule/xmodule/js/src/poll/poll.js
Normal file
@@ -0,0 +1,5 @@
|
||||
window.Poll = function (el) {
|
||||
RequireJS.require(['PollMain'], function (PollMain) {
|
||||
new PollMain(el);
|
||||
});
|
||||
};
|
||||
255
common/lib/xmodule/xmodule/js/src/poll/poll_main.js
Normal file
255
common/lib/xmodule/xmodule/js/src/poll/poll_main.js
Normal file
@@ -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(
|
||||
'<h3>Error!</h3>' +
|
||||
'<p>XML data format changed. List of answers was modified, but poll data was not updated.</p>'
|
||||
);
|
||||
|
||||
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 = $('<div />').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 = $('<div class="poll_answer"></div>');
|
||||
|
||||
answer.questionEl = $('<div class="question"></div>');
|
||||
answer.buttonEl = $('<div class="button"></div>');
|
||||
answer.textEl = $('<div class="text"></div>');
|
||||
answer.questionEl.append(answer.buttonEl);
|
||||
answer.questionEl.append(answer.textEl);
|
||||
|
||||
answer.el.append(answer.questionEl);
|
||||
|
||||
answer.statsEl = $('<div class="stats"></div>');
|
||||
answer.barEl = $('<div class="bar"></div>');
|
||||
answer.percentEl = $('<div class="percent"></div>');
|
||||
answer.barEl.append(answer.percentEl);
|
||||
answer.numberEl = $('<div class="number"></div>');
|
||||
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 = $('<div class="graph_answer"></div>');
|
||||
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));
|
||||
10
common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
Normal file
10
common/lib/xmodule/xmodule/js/src/wrapper/edit.coffee
Normal file
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
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': <List of answers>,
|
||||
'question': <Question string>
|
||||
}
|
||||
"""
|
||||
# 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from itertools import chain
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = '''<sequential display_name="oops"><video url="hi"></sequential>'''
|
||||
@@ -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'''
|
||||
|
||||
|
||||
65
common/lib/xmodule/xmodule/tests/test_logic.py
Normal file
65
common/lib/xmodule/xmodule/tests/test_logic.py
Normal file
@@ -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')
|
||||
26
common/lib/xmodule/xmodule/wrapper_module.py
Normal file
26
common/lib/xmodule/xmodule/wrapper_module.py
Normal file
@@ -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"
|
||||
|
||||
50
common/test/data/conditional_and_poll/README
Normal file
50
common/test/data/conditional_and_poll/README
Normal file
@@ -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 <course/> 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
|
||||
<course url_name="YEAR_SEMESTER" org="ORG" course="COURSENUM"/>
|
||||
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
|
||||
|
||||
-----------------------
|
||||
2
common/test/data/conditional_and_poll/README.md
Normal file
2
common/test/data/conditional_and_poll/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
content-harvard-justicex
|
||||
========================
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
|
||||
<section class="about">
|
||||
<h2>About ER22x</h2>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<!--
|
||||
<p>The assigned readings will be freely available online. They are also collected in an edited volume, <emph>Justice: A Reader</emph> (ed. Michael Sandel, Oxford University Press). Students who would like further guidance on the themes of the lectures can read Michael Sandel, <emph>Justice: What’s the Right Thing to Do?</emph> (Recommended but not required.)
|
||||
</p>
|
||||
-->
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section class="course-staff">
|
||||
<h2>Course instructor</h3>
|
||||
<article class="teacher">
|
||||
<!-- TODO: Need to change image location -->
|
||||
<!-- First Professor -->
|
||||
<div class="teacher-image"><img src="/static/images/professor-sandel.jpg"/></div>
|
||||
<h3>Michael J. Sandel</h3>
|
||||
<p>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 <i>What Money Can't Buy: The Moral Limits of Markets</i> (2012); <i>Justice: What's the Right Thing to Do?</i> (2009); <i>The Case against Perfection: Ethics in the Age of Genetic Engineering</i> (2007); <i>Public Philosophy: Essays on Morality in Politics</i> (2005); <i>Democracy's Discontent</i> (1996); and <i>Liberalism and the Limits of Justice</i>(1982; 2nd ed., 1998). </p>
|
||||
<p><br></p>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="faq">
|
||||
<section class="responses">
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<article class="response">
|
||||
<h3>How much does it cost to take the course?</h3>
|
||||
<p>Nothing! The course is free.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Does the course have any prerequisites?</h3>
|
||||
<p>No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Do I need any other materials to take the course?</h3>
|
||||
<p>No. As long as you’ve got a computer to access the website, you are ready to take the course.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Is there a textbook for the course?</h3>
|
||||
<p>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.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Do I need to watch the lectures at a specific time?</h3>
|
||||
<p>No. You can watch the lectures at your leisure.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Will I be able to participate in class discussions?</h3>
|
||||
<p>Yes, in several ways: </p>
|
||||
|
||||
<ol>
|
||||
<li><p> 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.</p></li>
|
||||
|
||||
<li><p> 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. </p></li>
|
||||
|
||||
<li><p> Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.</p></li>
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Will certificates be awarded?</h3>
|
||||
<p>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. </p>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
None
|
||||
@@ -0,0 +1 @@
|
||||
JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
|
||||
@@ -0,0 +1 @@
|
||||
<iframe width="560" height="315" src="http://www.youtube.com/embed/fajlZMdPkKE#!" frameborder="0" allowfullscreen></iframe>
|
||||
3
common/test/data/conditional_and_poll/chapter/Staff.xml
Normal file
3
common/test/data/conditional_and_poll/chapter/Staff.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<chapter>
|
||||
<sequential url_name="Problem_Demos"/>
|
||||
</chapter>
|
||||
1
common/test/data/conditional_and_poll/course.xml
Symbolic link
1
common/test/data/conditional_and_poll/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2013_Spring.xml
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- Name this file eg "2012_Fall.xml" or "2013_Spring.xml"
|
||||
Take note of this name exactly, you'll need to use it everywhere. -->
|
||||
<course>
|
||||
<chapter url_name="Staff"/>
|
||||
</course>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- A file named "course.xml" in your top-level should point
|
||||
at the appropriate roots file. You can do so like this:
|
||||
|
||||
$ rm course.xml
|
||||
$ ln -s roots/YEAR_SEMESTER.xml course.xml
|
||||
|
||||
|
||||
Ask Sarina for help with this. -->
|
||||
@@ -0,0 +1,3 @@
|
||||
<ol>
|
||||
<li>A list of course handouts, or an empty file if there are none.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- If you wish to make a welcome announcement -->
|
||||
<ol>
|
||||
|
||||
<li><h2>December 9</h2>
|
||||
<section class="update-description">
|
||||
<p>Announcement text</p>
|
||||
</section>
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"course/2013_Spring": {
|
||||
"start": "2099-01-01T00:00",
|
||||
"advertised_start" : "Spring 2013",
|
||||
"display_name": "Justice"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<sequential>
|
||||
<vertical>
|
||||
<poll_question id="first_poll" display_name="first poll">
|
||||
<h3>What's the Right Thing to Do?</h3>
|
||||
<p>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?</p>
|
||||
<answer id="Yes">Yes</answer>
|
||||
<answer id="No">No</answer>
|
||||
<answer id="Dont_know">Don't know</answer>
|
||||
</poll_question>
|
||||
<poll_question id="second_poll" display_name="second poll">
|
||||
<h3>What's the Right Thing to Do?</h3>
|
||||
<p>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?</p>
|
||||
<answer id="Yes">Yes</answer>
|
||||
<answer id="No">No</answer>
|
||||
<answer id="Dont_know">Don't know</answer>
|
||||
</poll_question>
|
||||
</vertical>
|
||||
|
||||
<wrapper>
|
||||
<!-- Test many show tags -->
|
||||
<html>Condition: first_poll - Yes</html>
|
||||
<conditional sources="i4x://HarvardX/ER22x/poll_question/first_poll" poll_answer="Yes">
|
||||
<html>In first condition.</html>
|
||||
<show sources="i4x://HarvardX/ER22x/poll_question/second_poll"/>
|
||||
</conditional>
|
||||
</wrapper>
|
||||
</sequential>
|
||||
5
common/test/data/conditional_and_poll/static/README
Normal file
5
common/test/data/conditional_and_poll/static/README
Normal file
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 443 KiB |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<div id="conditional_${element_id}" class="conditional-wrapper" data-problem-id="${id}" data-url="${ajax_url}"></div>
|
||||
<div
|
||||
id="conditional_${element_id}"
|
||||
class="conditional-wrapper"
|
||||
data-problem-id="${id}"
|
||||
data-url="${ajax_url}"
|
||||
data-depends="${depends}"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<%
|
||||
from django.core.urlresolvers import reverse
|
||||
reqm = module.required_modules[0]
|
||||
course_id = module.system.course_id
|
||||
condition = module.condition
|
||||
%>
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
<p><a href="${reverse('jump_to',kwargs=dict(course_id=course_id, location=reqm.location.url()))}">${reqm.display_name}</a>
|
||||
must be
|
||||
% if 'attempted' in condition:
|
||||
attempted
|
||||
% else:
|
||||
completed
|
||||
# course_id = module.location.course_id
|
||||
def get_course_id(module):
|
||||
return module.location.org +'/' + module.location.course +'/' + \
|
||||
module.system.ajax_url.split('/')[4]
|
||||
|
||||
def _message(reqm, message):
|
||||
return message.format(link="<a href={url}>{url_name}</a>".format(
|
||||
url = reverse('jump_to', kwargs=dict(course_id=get_course_id(reqm),
|
||||
location=reqm.location.url())),
|
||||
url_name = reqm.display_name))
|
||||
%>
|
||||
% if message:
|
||||
% for reqm in module.required_modules:
|
||||
<p>${_message(reqm, message)}</p>
|
||||
% endfor
|
||||
% endif
|
||||
before this will become visible.</p>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<div class='container' data-url="${ajax_url}">
|
||||
% if voted:
|
||||
<div>Upvotes: ${upvotes}</div>
|
||||
<div>Downvotes: ${downvotes}</div>
|
||||
% else:
|
||||
<a class="upvote">Yes</a>
|
||||
<a class="downvote">No</a>
|
||||
% endif
|
||||
</div>
|
||||
<section
|
||||
id="poll_${element_id}"
|
||||
class="${element_class}"
|
||||
data-ajax-url="${ajax_url}"
|
||||
>
|
||||
<!-- Hidden field to read configuration JSON from. -->
|
||||
<div class="${element_class}_div" id="${element_id}_json" style="display: none;">${configuration_json}</div>
|
||||
</section>
|
||||
6
rakefile
6
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user