Commit 059b9f66 by Alexander Kryklia

poll and conditional finished

parent 50af0be1
......@@ -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>&frasl;<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'
......
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;
}
}
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)
updateProgress: (response) =>
if response.progress_changed
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
@callerElId = callerElId
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
@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) =>
@el.html(response.html)
XModule.loadModules(@el)
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
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
// 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)
window.Poll = function (el) {
RequireJS.require(['PollMain'], function (PollMain) {
new PollMain(el);
});
};
(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));
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
......
"""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.
Args:
dispatch: string request slug
get: dict request get parameters
return json.dumps({'error': 'Unknown Command!'})
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(MakoModuleDescriptor, XmlDescriptor):
_tag_name = 'poll_question'
_child_tag_name = 'answer'
class PollDescriptor(RawDescriptor):
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': <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
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")
......
# -*- 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'''
......
# -*- 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')
# 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"
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
-----------------------
\ No newline at end of file
content-harvard-justicex
========================
\ No newline at end of file
<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>
\ No newline at end of file
JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
<iframe width="560" height="315" src="http://www.youtube.com/embed/fajlZMdPkKE#!" frameborder="0" allowfullscreen></iframe>
\ No newline at end of file
<chapter>
<sequential url_name="Problem_Demos"/>
</chapter>
roots/2013_Spring.xml
\ No newline at end of file
<!-- 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>
<!-- 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. -->
<ol>
<li>A list of course handouts, or an empty file if there are none.</li>
</ol>
<!-- 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>
{
"course/2013_Spring": {
"start": "2099-01-01T00:00",
"advertised_start" : "Spring 2013",
"display_name": "Justice"
}
}
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
<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>
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
......@@ -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;
......
<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>
<%
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
# course_id = module.location.course_id
def get_course_id(module):
return module.location.org +'/' + module.location.course +'/' + \
module.system.ajax_url.split('/')[4]
<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
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>
<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>
\ No newline at end of file
<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>
\ No newline at end of file
......@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment