adds initial word_cloud_module files
This commit is contained in:
committed by
Vasyl Nakvasiuk
parent
e5daeb41fb
commit
bb9bc42171
@@ -52,6 +52,7 @@ setup(
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
|
||||
"hidden = xmodule.hidden_module:HiddenDescriptor",
|
||||
"raw = xmodule.raw_module:RawDescriptor",
|
||||
],
|
||||
|
||||
54
common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
Normal file
54
common/lib/xmodule/xmodule/js/src/word_cloud/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)
|
||||
323
common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
Normal file
323
common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
Normal file
@@ -0,0 +1,323 @@
|
||||
(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;
|
||||
|
||||
console.log('submit answer');
|
||||
|
||||
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) {
|
||||
console.log('success! response = ');
|
||||
console.log(response);
|
||||
|
||||
_this.showAnswerGraph(response.poll_answers, response.total);
|
||||
|
||||
if (_this.canReset === true) {
|
||||
_this.resetButton.show();
|
||||
}
|
||||
|
||||
// Initialize Conditional constructors.
|
||||
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) {
|
||||
|
||||
|
||||
'submitReset': function () {
|
||||
var _this;
|
||||
|
||||
_this = this;
|
||||
|
||||
console.log('submit reset');
|
||||
|
||||
// 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 + '/' + 'reset_poll',
|
||||
{},
|
||||
function (response) {
|
||||
console.log('success! response = ');
|
||||
console.log(response);
|
||||
|
||||
if (
|
||||
(response.hasOwnProperty('status') !== true) ||
|
||||
(typeof response.status !== 'string') ||
|
||||
(response.status.toLowerCase() !== 'success')) {
|
||||
return;
|
||||
}
|
||||
|
||||
_this.questionAnswered = false;
|
||||
_this.questionEl.find('.button.answered').removeClass('answered');
|
||||
_this.questionEl.find('.stats').hide();
|
||||
_this.resetButton.hide();
|
||||
|
||||
// Initialize Conditional constructors. We will specify the third parameter as 'true'
|
||||
// notifying the constructor that this is a reset operation.
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(this.jsonConfig.reset);
|
||||
|
||||
if ((typeof this.jsonConfig.reset === 'string') && (this.jsonConfig.reset.toLowerCase() === 'true')) {
|
||||
this.canReset = true;
|
||||
|
||||
this.resetButton = $('<div class="button reset-button">Change your vote</div>');
|
||||
|
||||
if (this.questionAnswered === false) {
|
||||
this.resetButton.hide();
|
||||
}
|
||||
|
||||
this.resetButton.appendTo(this.questionEl);
|
||||
|
||||
this.resetButton.on('click', function () {
|
||||
_this.submitReset();
|
||||
});
|
||||
} else {
|
||||
this.canReset = false;
|
||||
}
|
||||
|
||||
// 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));
|
||||
@@ -0,0 +1,5 @@
|
||||
window.Poll = function (el) {
|
||||
RequireJS.require(['PollMain'], function (PollMain) {
|
||||
new PollMain(el);
|
||||
});
|
||||
};
|
||||
166
common/lib/xmodule/xmodule/word_cloud_module.py
Normal file
166
common/lib/xmodule/xmodule/word_cloud_module.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Word cloud is ungraded xblock used by students to
|
||||
generate and view word cloud..
|
||||
|
||||
On the client side we show:
|
||||
If student does not yet anwered - five text inputs.
|
||||
If student have answered - words he entered and cloud.
|
||||
|
||||
Stunent can 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
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
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 WordCloudFields(object):
|
||||
# Name of poll to use in links to this poll
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
|
||||
submitted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
|
||||
student_words= List(help="Student answer", scope=Scope.student_state, default=[])
|
||||
all_words = Object(help="All possible words from other students", scope=Scope.content)
|
||||
top_words = Object(help="Top N words for word cloud", scope=Scope.content)
|
||||
top_low_border = Int(help="Number to distinguish top from all words", scope=Scope.content)
|
||||
|
||||
class WordCloudModule(WordCloudFields, XModule):
|
||||
"""WordCloud Module"""
|
||||
js = {
|
||||
'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
|
||||
'js': [resource_string(__name__, 'js/src/word_cloud/logme.js'),
|
||||
resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
|
||||
resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
|
||||
}
|
||||
css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
|
||||
js_module_name = "Word_Cloud"
|
||||
|
||||
Number_of_top_words = 250
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""Ajax handler.
|
||||
|
||||
Args:
|
||||
dispatch: string request slug
|
||||
get: dict request get parameters
|
||||
|
||||
Returns:
|
||||
json string
|
||||
"""
|
||||
if dispatch == 'submit':
|
||||
|
||||
# self.all_words[word] -= 1
|
||||
# FIXME: fix this, when xblock will support mutable types.
|
||||
# Now we use this hack.
|
||||
# speed issues
|
||||
temp_all_words = self.all_words
|
||||
temp_top_words = self.top_words
|
||||
|
||||
if self.submitted:
|
||||
|
||||
for word in self.student_words:
|
||||
temp_all_words[word] -= 1
|
||||
|
||||
if word in temp_top_words:
|
||||
temp_top_words -= 1
|
||||
|
||||
else:
|
||||
self.submitted = True
|
||||
|
||||
self.student_words = get['student_words']
|
||||
|
||||
question_words = {}
|
||||
|
||||
for word in self.student_words:
|
||||
temp_all_words[word] += 1
|
||||
|
||||
if word in temp_top_words:
|
||||
temp_top_words += 1
|
||||
else:
|
||||
if temp_all_words[word] > top_low_border:
|
||||
question_words[word] = temp_all_words[word]
|
||||
|
||||
|
||||
self.all_words = temp_all_words
|
||||
|
||||
self.top_words = self.update_top_words(question_words, temp_top_words)
|
||||
|
||||
|
||||
return json.dumps({'student_words': self.student_words,
|
||||
'top_words': self.top_words,
|
||||
})
|
||||
elif dispatch == 'get_state':
|
||||
return json.dumps({'student_answers': self.student_answers,
|
||||
'top_words': self.top_words)
|
||||
})
|
||||
else: # return error message
|
||||
return json.dumps({'error': 'Unknown Command!'})
|
||||
|
||||
|
||||
def update_top_words(question_words, top_words):
|
||||
|
||||
for word, number in question_words:
|
||||
for top_word, top_number in top_words[:]:
|
||||
if top_number < number:
|
||||
del top_words[top_word]
|
||||
top_words[word] - number
|
||||
break
|
||||
|
||||
return top_words
|
||||
|
||||
def get_html(self):
|
||||
"""Renders parameters to template."""
|
||||
params = {
|
||||
'element_id': self.location.html_id(),
|
||||
'element_class': self.location.category,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'configuration_json': json.dumps({}),
|
||||
}
|
||||
self.content = self.system.render_template('word_cloud.html', params)
|
||||
return self.content
|
||||
|
||||
|
||||
|
||||
class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
_tag_name = 'word_cloud'
|
||||
|
||||
module_class = WordCloudModule
|
||||
template_dir_name = 'word_cloud'
|
||||
stores_state = True
|
||||
|
||||
@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 = {}
|
||||
children = []
|
||||
|
||||
return (definition, children)
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
"""Return an xml element representing to this definition."""
|
||||
poll_str = '<{tag_name}/>'.format(tag_name=self._tag_name)
|
||||
xml_object = etree.fromstring(poll_str)
|
||||
xml_object.set('display_name', self.display_name)
|
||||
|
||||
return xml_object
|
||||
8
lms/templates/word_cloud.html
Normal file
8
lms/templates/word_cloud.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user