Commit d55bbebf by solashirai

initial commit

parents
*.pyc
*~
*.egg-info/
This is the repository for the Crowdsource Hinter XBlock. The Crowdsource Hinter serves to provide students with hints when they incorrectly answer a problem within a course. The hinter is compatible with numerical input and text input type problems.
This XBlock is still a prototype.
An example of a student recieving a hint
![CrowdSourceHinter Hint Screenshot](crowdsourcehinter_hint.png)
An example of after a student corrects their answer
![CrowdSourceHinter Screenshot](crowdsourcehinter_correct.png)
To bring the crowd sourced hinter into a demo course:
Follow https://github.com/edx/edx-platform/wiki/Installing-a-new-XBlock for basic xblock installation. The name of the module to set in the advanced settings tab is "crowdsourcehinter".
In studio view, edit the hinter so that the "Problem Element" is set to the "data-usage-id" of the problem block (findable by inspecting element of the problem block). If no problem element is set manually, the hinter will default to respond to the first problem block on the page.
![CrowdSourceHinter Installation Screenshot](crowdsourcehinter_setup.png)
The two key features of the crowdsource hinter are the abilities to show students hints and to have the students themselves create hints to be shown to future students.
When a student incorrectly answers a problem, the hinter will look through its database to search for a hint that has been stored for that exact incorrect answer input (i.e. when the database is large enough, two different incorrect answers would not receive the same hint). If hints exist for a student's incorrect answer, this hint is shown to the student. The student then may have the opportunity to input their answer again, which may prompt another hint to be displayed.
After a student re-submits an answer correctly, they can rate hints for their usefulness or contribute a new hint to be used by other students. Rating hints works by upvoting, downvoting, or reporting hints. The new hint that is contributed by a student is specific to the incorrect answer that they make (currently the first incorrect answer will be prompted for contributing new hints).
from .crowdsourcehinter import CrowdsourceHinter
import ast
import logging
import operator
import pkg_resources
import random
import json
import copy
from xblock.core import XBlock
from xblock.fields import Scope, Dict, List, Boolean, String
from xblock.fragment import Fragment
log = logging.getLogger(__name__)
class CrowdsourceHinter(XBlock):
"""
This is the Crowdsource Hinter XBlock. This Xblock seeks to provide students with hints
that specifically address their mistake. Additionally, the hints that this Xblock shows
are created by the students themselves. This doc string will probably be edited later.
"""
# Database of hints. hints are stored as such: {"incorrect_answer": {"hint": rating}}. each key (incorrect answer)
# has a corresponding dictionary (in which hints are keys and the hints' ratings are the values).
#
# Example: {"computerr": {"You misspelled computer, remove the last r.": 5}}
hint_database = Dict(default={}, scope=Scope.user_state_summary)
# Database of initial hints, set by the course instructor. hint_database will receive the hints inputted
# in initial_hints. Initial hints have a default rating of 0.
#
# Example: {"Jeorge Washington": "You spelled his first name wrong."}
initial_hints = Dict(default={}, scope=Scope.content)
# This is a list of incorrect answer submissions made by the student. this list is mostly used for
# when the student starts rating hints, to find which incorrect answer's hint a student voted on.
#
# Example: ["personal computer", "PC", "computerr"]
incorrect_answers = List([], scope=Scope.user_state)
# A dictionary of generic_hints. default hints will be shown to students when there are no matches with the
# student's incorrect answer within the hint_database dictionary (i.e. no students have made hints for the
# particular incorrect answer)
#
# Example: ["Make sure to check your answer for simple mistakes like typos!"]
generic_hints = List(default=[], scope=Scope.content)
# List of which hints have been shown to the student
# this list is used to prevent the same hint from showing up to a student (if they submit the same incorrect answers
# multiple times)
#
# Example: ["You misspelled computer, remove the last r."]
used = List([], scope=Scope.user_state)
# This is a dictionary of hints that have been reported. the values represent the incorrect answer submission, and the
# keys are the hints the corresponding hints. hints with identical text for differing answers will all not show up for the
# student.
#
# Example: {"desk": "You're completely wrong, the answer is supposed to be computer."}
reported_hints = Dict(default={}, scope=Scope.user_state_summary)
# This String represents the xblock element for which the hinter is running. It is necessary to manually
# set this value in the XML file under the format "hinting_element": "i4x://edX/DemoX/problem/Text_Input" .
# Setting the element in the XML file is critical for the hinter to work.
#
# TODO: probably should change the name from Element (problem_element? hinting_element?). Trying to
# just change the name didn't seem to operate properly, check carefully what is changed
Element = String(default="", scope=Scope.content)
def studio_view(self, context=None):
"""
This function defines a view for editing the XBlock when embedding it in a course. It will allow
one to define, for example, which problem the hinter is for. It is unfinished and does not currently
work.
"""
html = self.resource_string("static/html/crowdsourcehinterstudio.html")
frag = Fragment(html)
frag.add_javascript_url('//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.8.1/mustache.min.js')
frag.add_css(self.resource_string("static/css/crowdsourcehinter.css"))
frag.add_javascript(self.resource_string("static/js/src/crowdsourcehinter_studio.js"))
frag.initialize_js('CrowdsourceHinterStudio', {'initial': str(self.initial_hints), 'generic': str(self.generic_hints), 'element': str(self.Element)})
return frag
@XBlock.json_handler
def set_initial_settings(self, data, suffix=''):
"""
Set intial hints, generic hints, and problem element from the studio view.
"""
initial = ast.literal_eval(str(data['initial_hints']))
generic = ast.literal_eval(str(data['generic_hints']))
if type(generic) is list and type(initial) is dict:
self.initial_hints = initial
self.generic_hints = generic
self.Element = str(data['element'])
return {'success': True}
return {'success': False}
def resource_string(self, path):
"""
This function is used to get the path of static resources.
"""
data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8")
def get_user_is_staff(self):
"""
Return self.xmodule_runtime.user_is_staff
This is not a supported part of the XBlocks API. User data is still
being defined. However, It's the only way to get the data right now.
"""
return self.xmodule_runtime.user_is_staff
def student_view(self, context=None):
"""
This view renders the hint view to the students. The HTML has the hints templated
in, and most of the remaining functionality is in the JavaScript.
"""
html = self.resource_string("static/html/crowdsourcehinter.html")
frag = Fragment(html)
frag.add_javascript_url('//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.8.1/mustache.min.js')
frag.add_css(self.resource_string("static/css/crowdsourcehinter.css"))
frag.add_javascript(self.resource_string("static/js/src/crowdsourcehinter.js"))
frag.initialize_js('CrowdsourceHinter', {'hinting_element': self.Element, 'isStaff': self.xmodule_runtime.user_is_staff})
return frag
@XBlock.json_handler
def get_hint(self, data, suffix=''):
"""
Returns hints to students. Hints with the highest rating are shown to students unless the student has already
submitted the same incorrect answer previously.
Args:
data['submittedanswer']: The string of text that the student submits for a problem.
returns:
'BestHint': the highest rated hint for an incorrect answer
or another random hint for an incorrect answer
or 'Sorry, there are no hints for this answer.' if no hints exist
'StudentAnswer': the student's incorrect answer
"""
# populate hint_database with hints from initial_hints if there are no hints in hint_database.
# this probably will occur only on the very first run of a unit containing this block.
for answers in self.initial_hints:
if answers not in self.hint_database:
self.hint_database[answers] = {}
if self.initial_hints[answers] not in self.hint_database[answers]:
self.hint_database[answers].update({self.initial_hints[answers]: 0})
answer = str(data["submittedanswer"])
# put the student's answer to lower case so that differences in capitalization don't make
# different groups of hints. this is sloppy and the execution should probably be changed.
answer = answer.lower()
found_equal_sign = 0
remaining_hints = int(0)
best_hint = ""
# the string returned by the event problem_graded is very messy and is different
# for each problem, but after all of the numbers/letters there is an equal sign, after which the
# student's input is shown. I use the function below to remove everything before the first equal
# sign and only take the student's actual input.
#
# TODO: figure out better way to directly get text of student's answer
if "=" in answer:
if found_equal_sign == 0:
found_equal_sign = 1
eqplace = answer.index("=") + 1
answer = answer[eqplace:]
remaining_hints = str(self.find_hints(answer))
if remaining_hints != str(0):
for hint in self.hint_database[str(answer)]:
if hint not in self.reported_hints.keys():
# if best_hint hasn't been set yet or the rating of hints is greater than the rating of best_hint
if best_hint == "" or self.hint_database[str(answer)][hint] > self.hint_database[str(answer)][str(best_hint)]:
best_hint = hint
self.used.append(best_hint)
return {'BestHint': best_hint, "StudentAnswer": answer}
# find generic hints for the student if no specific hints exist
if len(self.generic_hints) != 0:
generic_hint = random.choice(self.generic_hints)
self.used.append(generic_hint)
return {'BestHint': generic_hint, "StudentAnswer": answer}
else:
# if there are no hints in either the database or generic hints
self.used.append(str("There are no hints for" + " " + answer))
return {'BestHint': "Sorry, there are no hints for this answer.", "StudentAnswer": answer}
def find_hints(self, answer):
"""
This function is used to check that an incorrect answer has available hints to show.
It will also add the incorrect answer test to self.incorrect_answers.
Args:
answer: This is equal to answer from get_hint, the answer the student submitted
Returns 0 if no hints to show exist
"""
isreported = []
self.incorrect_answers.append(str(answer))
if str(answer) not in self.hint_database:
# add incorrect answer to hint_database if no precedent exists
self.hint_database[str(answer)] = {}
return str(0)
for hint_keys in self.hint_database[str(answer)]:
for reported_keys in self.reported_hints:
if hint_keys == reported_keys:
isreported.append(hint_keys)
if (len(self.hint_database[str(answer)]) - len(isreported)) > 0:
return str(1)
else:
return str(0)
@XBlock.json_handler
def get_used_hint_answer_data(self, data, suffix=''):
"""
This function helps to facilitate student rating of hints and contribution of new hints.
Specifically this function is used to send necessary data to JS about incorrect answer
submissions and hints. It also will return hints that have been reported, although this
is only for Staff.
Returns:
used_hint_answer_text: This dicitonary contains reported hints/answers (if the user is staff) and the
first hint/answer pair that the student submitted for a problem.
"""
# used_hint_answer_text is a dictionary of hints (or lack thereof) used for a
# specific answer, as well as 2 other random hints that exist for each answer
# that were not used. The keys are the used hints, the values are the
# corresponding incorrect answer
used_hint_answer_text = {}
if self.get_user_is_staff():
for key in self.reported_hints:
used_hint_answer_text[key] = str("Reported")
if len(self.incorrect_answers) == 0:
return used_hint_answer_text
else:
for index in range(0, len(self.used)):
# each index is a hint that was used, in order of usage
if str(self.used[index]) in self.hint_database[self.incorrect_answers[index]]:
# add new key (hint) to used_hint_answer_text with a value (incorrect answer)
used_hint_answer_text[str(self.used[index])] = str(self.incorrect_answers[index])
self.incorrect_answers = []
self.used = []
return used_hint_answer_text
else:
# if the student's answer had no hints (or all the hints were reported and unavailable) return None
used_hint_answer_text[None] = str(self.incorrect_answers[index])
self.incorrect_answers = []
self.used = []
return used_hint_answer_text
self.incorrect_answers = []
self.used = []
return used_hint_answer_text
@XBlock.json_handler
def rate_hint(self, data, suffix=''):
"""
Used to facilitate hint rating by students.
Hint ratings in hint_database are updated and the resulting hint rating (or reported status) is returned to JS.
Args:
data['student_answer']: The incorrect answer that corresponds to the hint that is being rated
data['hint']: The hint that is being rated
data['student_rating']: The rating chosen by the student.
Returns:
'rating': the new rating of the hint, or the string 'reported' if the hint was reported
'hint': the hint that had its rating changed
"""
answer_data = data['student_answer']
data_rating = data['student_rating']
data_hint = data['hint']
if data_hint == 'Sorry, there are no hints for this answer.':
return {"rating": None, 'hint': data_hint}
if data['student_rating'] == 'unreport':
for reported_hints in self.reported_hints:
if reported_hints == data_hint:
self.reported_hints.pop(data_hint, None)
return {'rating': 'unreported'}
if data['student_rating'] == 'remove':
for reported_hints in self.reported_hints:
if data_hint == reported_hints:
self.hint_database[self.reported_hints[data_hint]].pop(data_hint, None)
self.reported_hints.pop(data_hint, None)
return {'rating': 'removed'}
if data['student_rating'] == 'report':
# add hint to Reported dictionary
self.reported_hints[str(data_hint)] = answer_data
return {"rating": 'reported', 'hint': data_hint}
rating = self.change_rating(data_hint, data_rating, answer_data)
return {"rating": str(rating), 'hint': data_hint}
def change_rating(self, data_hint, data_rating, answer_data):
"""
This function is used to change the rating of a hint when students vote on its helpfulness.
Initiated by rate_hint. The temporary_dictionary is manipulated to be used
in self.rate_hint
Args:
data_hint: This is equal to the data['hint'] in self.rate_hint
data_rating: This is equal to the data['student_rating'] in self.rate_hint
answer_data: This is equal to the data['student_answer'] in self.rate_hint
Returns:
The rating associated with the hint is returned. This rating is identical
to what would be found under self.hint_database[answer_string[hint_string]]
"""
if any(data_hint in generic_hints for generic_hints in self.generic_hints):
return
if data_rating == 'upvote':
self.hint_database[str(answer_data)][str(data_hint)] += 1
return self.hint_database[str(answer_data)][str(data_hint)]
else:
self.hint_database[str(answer_data)][str(data_hint)] -= 1
return self.hint_database[str(answer_data)][str(data_hint)]
@XBlock.json_handler
def add_new_hint(self, data, suffix=''):
"""
This function adds a new hint submitted by the student into the hint_database.
Args:
data['submission']: This is the text of the new hint that the student has submitted.
data['answer']: This is the incorrect answer for which the student is submitting a new hint.
"""
submission = data['submission']
answer = data['answer']
if str(submission) not in self.hint_database[str(answer)]:
self.hint_database[str(answer)].update({submission: 0})
return
else:
# if the hint exists already, simply upvote the previously entered hint
if str(submission) in self.generic_hints:
return
else:
self.hint_database[str(answer)][str(submission)] += 1
return
@XBlock.json_handler
def studiodata(self, data, suffix=''):
"""
This function serves to return the dictionary of reported hints to JS. This is intended for use in
the studio_view, which is under construction at the moment
"""
return self.reported_hints
@staticmethod
def workbench_scenarios():
"""A canned scenario for display in the workbench."""
return [
("CrowdsourceHinter",
"""
<verticaldemo>
<crowdsourcehinter>
{"generic_hints": "Make sure to check for basic mistakes like typos", "initial_hints": {"michiganp": "remove the p at the end.", "michigann": "too many Ns on there."}, "hinting_element": "i4x://edX/DemoX/problem/Text_Input"}
</crowdsourcehinter>
</verticaldemo>
"""
)
]
@classmethod
def parse_xml(cls, node, runtime, keys, _id_generator):
"""
A minimal working test for parse_xml
"""
block = runtime.construct_xblock_from_class(cls, keys)
xmlText = ast.literal_eval(str(node.text))
if xmlText:
block.generic_hints.append(str(xmlText["generic_hints"]))
block.initial_hints = copy.copy(xmlText["initial_hints"])
block.Element = str(xmlText["hinting_element"])
return block
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false*/
(function (global, factory) {
if (typeof exports === "object" && exports) {
factory(exports); // CommonJS
} else if (typeof define === "function" && define.amd) {
define(['exports'], factory); // AMD
} else {
factory(global.Mustache = {}); // <script>
}
}(this, function (mustache) {
var Object_toString = Object.prototype.toString;
var isArray = Array.isArray || function (object) {
return Object_toString.call(object) === '[object Array]';
};
function isFunction(object) {
return typeof object === 'function';
}
function escapeRegExp(string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
var RegExp_test = RegExp.prototype.test;
function testRegExp(re, string) {
return RegExp_test.call(re, string);
}
var nonSpaceRe = /\S/;
function isWhitespace(string) {
return !testRegExp(nonSpaceRe, string);
}
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var equalsRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!/;
/**
* Breaks up the given `template` string into a tree of tokens. If the `tags`
* argument is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
* course, the default is to use mustaches (i.e. mustache.tags).
*
* A token is an array with at least 4 elements. The first element is the
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
* all text that appears outside a symbol this element is "text".
*
* The second element of a token is its "value". For mustache tags this is
* whatever else was inside the tag besides the opening symbol. For text tokens
* this is the text itself.
*
* The third and fourth elements of the token are the start and end indices,
* respectively, of the token in the original template.
*
* Tokens that are the root node of a subtree contain two more elements: 1) an
* array of tokens in the subtree and 2) the index in the original template at
* which the closing tag for that section begins.
*/
function parseTemplate(template, tags) {
if (!template)
return [];
var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var nonSpace = false; // Is there a non-space char on the current line?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
function stripSpace() {
if (hasTag && !nonSpace) {
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
function compileTags(tags) {
if (typeof tags === 'string')
tags = tags.split(spaceRe, 2);
if (!isArray(tags) || tags.length !== 2)
throw new Error('Invalid tags: ' + tags);
openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
}
compileTags(tags || mustache.tags);
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;
// Match any text between tags.
value = scanner.scanUntil(openingTagRe);
if (value) {
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
nonSpace = true;
}
tokens.push([ 'text', chr, start, start + 1 ]);
start += 1;
// Check for whitespace on the current line.
if (chr === '\n')
stripSpace();
}
}
// Match the opening tag.
if (!scanner.scan(openingTagRe))
break;
hasTag = true;
// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);
// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') {
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
type = '&';
} else {
value = scanner.scanUntil(closingTagRe);
}
// Match the closing tag.
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
token = [ type, value, start, scanner.pos ];
tokens.push(token);
if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
compileTags(value);
}
}
// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens(tokens) {
var squashedTokens = [];
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* Forms the given array of `tokens` into a nested tree structure where
* tokens that represent a section have two additional items: 1) an array of
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
function nestTokens(tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[4] = [];
break;
case '/':
section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
/**
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
*/
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
return '';
var string = match[0];
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
Scanner.prototype.scanUntil = function (re) {
var index = this.tail.search(re), match;
switch (index) {
case -1:
match = this.tail;
this.tail = "";
break;
case 0:
match = "";
break;
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
this.pos += match.length;
return match;
};
/**
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
function Context(view, parentContext) {
this.view = view == null ? {} : view;
this.cache = { '.': this.view };
this.parent = parentContext;
}
/**
* Creates a new context using the given view with this context
* as the parent.
*/
Context.prototype.push = function (view) {
return new Context(view, this);
};
/**
* Returns the value of the given name in this context, traversing
* up the context hierarchy if the value is absent in this context's view.
*/
Context.prototype.lookup = function (name) {
var cache = this.cache;
var value;
if (name in cache) {
value = cache[name];
} else {
var context = this, names, index;
while (context) {
if (name.indexOf('.') > 0) {
value = context.view;
names = name.split('.');
index = 0;
while (value != null && index < names.length)
value = value[names[index++]];
} else {
value = context.view[name];
}
if (value != null)
break;
context = context.parent;
}
cache[name] = value;
}
if (isFunction(value))
value = value.call(this.view);
return value;
};
/**
* A Writer knows how to take a stream of tokens and render them to a
* string, given a context. It also maintains a cache of templates to
* avoid the need to parse the same template twice.
*/
function Writer() {
this.cache = {};
}
/**
* Clears all cached templates in this writer.
*/
Writer.prototype.clearCache = function () {
this.cache = {};
};
/**
* Parses and caches the given `template` and returns the array of tokens
* that is generated from the parse.
*/
Writer.prototype.parse = function (template, tags) {
var cache = this.cache;
var tokens = cache[template];
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* High-level method that is used to render the given `template` with
* the given `view`.
*
* The optional `partials` argument may be an object that contains the
* names and templates of partials that are used in the template. It may
* also be a function that is used to load partial templates on the fly
* that takes a single argument: the name of the partial.
*/
Writer.prototype.render = function (template, view, partials) {
var tokens = this.parse(template);
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* Low-level method that renders the given array of `tokens` using
* the given `context` and `partials`.
*
* Note: The `originalTemplate` is only ever used to extract the portion
* of the original template that was contained in a higher-order section.
* If the template doesn't use higher-order sections, this argument may
* be omitted.
*/
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
var buffer = '';
// This function is used to render an arbitrary template
// in the current context by higher-order sections.
var self = this;
function subRender(template) {
return self.render(template, context, partials);
}
var token, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
value = context.lookup(token[1]);
if (!value)
continue;
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
}
} else if (typeof value === 'object' || typeof value === 'string') {
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
} else if (isFunction(value)) {
if (typeof originalTemplate !== 'string')
throw new Error('Cannot use higher-order sections without the original template');
// Extract the portion of the original template that the section contains.
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
break;
case '^':
value = context.lookup(token[1]);
// Use JavaScript's definition of falsy. Include empty arrays.
// See https://github.com/janl/mustache.js/issues/186
if (!value || (isArray(value) && value.length === 0))
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
break;
case '>':
if (!partials)
continue;
value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
buffer += this.renderTokens(this.parse(value), context, partials, value);
break;
case '&':
value = context.lookup(token[1]);
if (value != null)
buffer += value;
break;
case 'name':
value = context.lookup(token[1]);
if (value != null)
buffer += mustache.escape(value);
break;
case 'text':
buffer += token[1];
break;
}
}
return buffer;
};
mustache.name = "mustache.js";
mustache.version = "0.8.1";
mustache.tags = [ "{{", "}}" ];
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();
/**
* Clears all cached templates in the default writer.
*/
mustache.clearCache = function () {
return defaultWriter.clearCache();
};
/**
* Parses and caches the given template in the default writer and returns the
* array of tokens it contains. Doing this ahead of time avoids the need to
* parse templates on the fly as they are rendered.
*/
mustache.parse = function (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* Renders the `template` with the given `view` and `partials` using the
* default writer.
*/
mustache.render = function (template, view, partials) {
return defaultWriter.render(template, view, partials);
};
// This is here for backwards compatibility with 0.4.x.
mustache.to_html = function (template, view, partials, send) {
var result = mustache.render(template, view, partials);
if (isFunction(send)) {
send(result);
} else {
return result;
}
};
// Export the escaping function so that the user may override it.
// See https://github.com/janl/mustache.js/issues/244
mustache.escape = escapeHtml;
// Export these mainly for testing, but also for advanced usage.
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
}));
This static directory is for files that should be included in your kit as plain
static files.
You can ask the runtime for a URL that will retrieve these files with:
url = self.runtime.local_resource_url(self, "static/js/lib.js")
The default implementation is very strict though, and will not serve files from
the static directory. It will serve files from a directory named "public".
Create a directory alongside this one named "public", and put files there.
Then you can get a url with code like this:
url = self.runtime.local_resource_url(self, "public/js/lib.js")
The sample code includes a function you can use to read the content of files
in the static directory, like this:
frag.add_javascript(self.resource_string("static/js/my_block.js"))
/* CSS for Crowdsource Hinter */
.crowdsourcehinter_block .csh_feedback {
}
.crowdsourcehinter_block .csh_vote {
padding-top: 2px;
padding-bottom: 2px;
}
.csh_reveal_info{
color: blue;
}
.csh_reveal_info:hover{
color: blue;
text-decoration: underline;
}
.csh_hint_value{
display: flex;
/* margin-left: 10px; */
flex-direction: column;
}
.crowdsourcehinter_block {
}
.csh_hint_reveal{
display: flex;
flex-direction: row;
justify-content: space-between;
}
.csh_hint_rating_on_incorrect{
display: flex;
flex-direction: row;
justify-content: space-between;
flex-shrink: 0;
}
.csh_hint_data{
display: flex;
flex-direction: column;
}
.csh_rating_data {
display: flex;
flex-direction: row;
align-self: flex-end;
flex-shrink: 0;
}
.csh_rate_hint, .csh_report_hint, .csh_rate_hint_completed, .csh_rate_hint_text {
align-self: flex-end;
margin-left: 10px;
}
div[data-rate="report"]{
font-weight: bold;
}
.csh_hint_text[rating="upvote"]{
color: green;
}
.csh_hint_text[rating="downvote"]{
color: red;
}
.csh_hint[rating="upvote"]{
color: green;
}
.csh_hint[rating="downvote"]{
color: red;
}
.csh_student_hint_creation {
height: 40px;
vertical-align: middle;
font-weight: 600;
align-self: flex-end;
box-shadow: 0px 0px 8px 4px #C7BEBE inset, 0px 0px 8px 4px #C7BEBE inset;
background-clip: padding-box;
font-size: 0.8125em;
}
.csh_rate_hint[data-rate="upvote"] {
color: green;
font-weight: bold;
transition: background .4s ease-in
}
.csh_rate_hint[data-rate="upvote"]:hover {
background-color: #BCBCBC;
}
.csh_rate_hint[data-rate="downvote"] {
color: red;
font-weight: bold;
transition: background .4s ease-in
}
.csh_rate_hint[data-rate="downvote"]:hover {
background-color: #BCBCBC;
}
.csh_report_hint {
transition: background .4s ease-in
}
.csh_report_hint:hover {
background-color: #BCBCBC;
}
.csh_reported_hints {
background-color: red;
}
.crowdsourcehinter_block .csh_reported_hints {
visibility: hidden;
display: none;
}
.crowdsourcehinter_block_is_staff .csh_reported_hints {
visibility: visible;
}
.csh_user_info{
padding-left: 22px ;
text-indent: -22px ;
}
.csh_rating{
margin-right: 5px;
}
.csh_rate_hint{ cursor: pointer }
.csh_report_hint{ cursor: pointer }
.csh_staff_rate{ cursor: pointer }
.csh_rate_hint{ color: #948f8f; }
.csh_reveal_info{ cursor: pointer }
.csh_hintsarea {
display: flex;
flex-direction: colomn;
}
.csh_student_text_input{
width: 100%;
}
<script type='x-tmpl/mustache' id='show_hint_rating_ux'>
<div class='csh_hint_value' value="{{hintText}}">
<div class='csh_hint_data'>
<div class="csh_hint">You received the following hint: <b>{{hintText}}</b></div>
</div>
<div class='csh_rating_data'>
<div role="button" class="csh_rate_hint" data-rate="upvote">
<b>Rate as Helpful</b>
</div>
<div role="button" class="csh_rate_hint" data-rate="downvote">
<b>Rate as Unhelpful</b>
</div>
<div role="button" class="csh_report_hint" data-icon="report" title="Report this hint.">
<b></b>
</div>
</div>
</div>
</script>
<script type="x-tmpl/mustache" id="show_reported_moderation">
<div class="csh_hint_value" value ="{{reportedHintText}}">
<div class="csh_hint">{{reportedHintText}}</div>
<div role="button" class="csh_staff_rate" data-rate="unreport" aria-label="unreport">
<u><b>Return hint for use in the hinter</b></u>
</div>
<div role="button" class="csh_staff_rate" data-rate="remove" aria-label="remove">
<u><b>Permanently remove hint.</b></u>
</div>
<div>--------</div>
</div>
</script>
<script type="x-tmpl/mustache" id="hint_text_input">
<p>
<input type="text" name="studentinput" class="csh_student_text_input">
</p>
<p>
<input answer="{{student_answer}}" type="button" class="csh_submit_new" value="Submit Hint">
</p>
</script>
<script type="x-tmpl/mustache" id="show_no_hints">
<div class="csh_hint_value"> This specific mistake doesn't appear to have any hints yet. You can contribute a hint to help other students who make the same mistake in the future.
</div>
</script>
<script type="x-tmpl/mustache" id="show_student_submission">
<div class="csh_student_answer">
<h class="csh_answer_text" answer={{answer}}>
<div role="button" class="csh_reveal_info">Help us improve hints for this problem </div>
<div class="csh_user_info" style="display: none;"> <br> Submitting an incorrect answer for this problem provides you with a hint specific to the mistake that you made - these hints are made by other students who made the same mistake as you did. Help to make the hints for this problem better by rating the hint that you received, or if you might have better advice to give other students, submit a new hint! <br> If you choose to submit a new hint, keep in mind that the hints are specific to one incorrect answer; specific advice (perhaps on some aspect of the problem that you overlooked) is more helpful than generic advice.</div>
<br>
Your original answer was: <b>{{answer}}</b></h>
</div>
</script>
<script type="x-tmpl/mustache" id="add_hint_creation">
<div>
<input type ="button" class="csh_student_hint_creation" value="Contribute a New Hint">
</input>
</div>
</script>
<div class="crowdsourcehinter_block">
<div class='csh_hint_reveal'>
<div class='csh_hint_text' student_answer = '' hint_received='' rating=''>
</div>
<div class='csh_hint_rating_on_incorrect'>
<div class="csh_rate_hint_text">
This hint was:
</div>
<div role="button" class="csh_rate_hint" data-rate="upvote" title="This hint was helpful!">
<b> Helpful </b>
</div>
<div role="button" class="csh_rate_hint" data-rate="downvote" title="This hint was not very helpful.">
<b> Unhelpful </b>
</div>
<div role="button" class="csh_report_hint" title="Report this hint">
<b></b>
</div>
</div>
</div>
<section class="csh_correct"></section>
<div class="csh_student_submission">
<div class="csh_reported_hints">
<span>moderate reported hints</span>
</div>
</div>
</div>
<div class="crowdsourcehinter_edit_block">
<p>
Generic hints should be in the form of a python list. e.x. ['generic hint text']
</p>
<p>
Generic Hints: <textarea type="text" class="csh_generic_hints" value = "{{generic}}"/>
</p>
<p>
Initial hints should be a python dictionary, with keys representing anticipated incorrect answers and values being hints. e.x {"incorrect answer example 1": "hint for incorrect answer 1"}. The rating of initial hints defaults to 0.
</p>
<p>
Initial Hints: <textarea type="text" class="csh_initial_hints" value = "{{initial}}"/>
</p>
<p>
This is the element of the problem for which the hinter is working. This is the "data-usage-id" of the problem block. It should look something like i4x://edX/DemoX/problem/f958789435cf47218ff32f0d600f1184
</p>
<p>
Problem Element: <textarea type="text" class="csh_hinting_element" value = "{{hinting_element}}"/>
</p>
<p>
<input type="button" class="csh_apply_settings" value="Apply Settings">
</p>
<div class='csh_check_success'/>
</div>
function CrowdsourceHinter(runtime, element, data){
var onHinterPage = true; //We don't do hinter logic if we're on a differ tab in a sequential.
$(".crowdsourcehinter_block", element).hide();
if(!onHinterPage){
return;
}
/**
* Set onHinterPage to false, disabling the hinter xblock. Triggered by switching units
* in edX course.
* This is a workaround for when a student switches to/from a unit and causes multiple
* instances of the hinter to be running.
*/
function stopScript(){
onHinterPage = false;
}
Logger.listen('seq_next', null, stopScript);
Logger.listen('seq_prev', null, stopScript);
Logger.listen('seq_goto', null, stopScript);
/**
* Get a hint from the server to show to the student after inc-orrectly answering a
* question. On success, continue to showHint.
* @param problemGradedEvent is data generated by the problem_graded event
*/
function getHint(problemGradedEvent){
$(".crowdsourcehinter_block", element).show();
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'get_hint'),
data: JSON.stringify({"submittedanswer": unescape(problemGradedEvent[0])}),
success: showHint
});
}
/**
* Start student hint rating/contribution. This will allow students to contribute new hints
* to the hinter as well as vote on the helpfulness of the first hint they received
* for the current problem. This function is called after the student answers
* the question correctly.
*/
function startHintRating(){
$('.csh_correct', element).show();
$(".csh_hint_reveal", element).hide();
if($('.csh_hint_creation', element)){
//send empty data for ajax call because not having a data field causes error
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'get_used_hint_answer_data'),
data: JSON.stringify({}),
success: setHintRatingUX
});
}
}
/**
* Check whether or not the question was correctly answered by the student.
* The current method of checking the correctness of the answer is very brittle
* since we simply look for a string within the problemGradedEventData.
* HACK
* @param problemGradedEventData is the data from problem_graded event.
*/
function checkIsAnswerCorrect(problemGradedEventData){
if (problemGradedEventData[1].search(/class="correct/) === -1){
return false;
} else {
return true;
}
}
/**
* Check whether student answered the question correctly and call the appropriate
* function afterwards. Current method for determining correctness if very brittle.
* @param event_type, element are both unused but automatically passed
* @param data is generated by problem_graded event, contains status and data of the problem block
*/
function onStudentSubmission(){ return function(event_type, data, element){
//search method of correctness of problem is brittle
if (checkIsAnswerCorrect(data)){
startHintRating();
} else { //if the submitted answer is incorrect
getHint(data);
}
}}
/**
* Set the target problem for which to listen for the problem_graded event. Set target to first
* problem block if no hinting element has been manually entered.
*/
if(data.hinting_element == undefined || data.hinting_element == ''){
//contains workaround because the data-usage-id shows up with ";_" in place of "/" in lms
hintingElement = ($('.xblock[data-block-type="problem"]').first().attr('data-usage-id')).replace(/;_/g, '/');
} else {
hintingElement = data.hinting_element;
}
Logger.listen('problem_graded', hintingElement, onStudentSubmission());
/**
* Modify csh_hint_text attributes to show hint to the student.
*/
function showHint(result){
$('.csh_hint_text', element).attr('student_answer', result.StudentAnswer);
$('.csh_hint_text', element).attr('hint_received', result.BestHint);
$('.csh_hint_text', element).text("Hint: " + result.BestHint);
$('.csh_rate_hint_completed', element).attr('class', 'csh_rate_hint');
$('.csh_hint_text', element).attr('rating', '');
Logger.log('crowd_hinter.showHint', {"student_answer": result.StudentAnswer, "hint_received": result.Hints});
}
/**
* Called by setHintRatingUX to append hints into divs created by
* showStudentSubmissoinHistory, after the student answered the question correctly.
* Feedback on hints at this stage consists of upvote/downvote/report buttons.
* @param hint is the first hint that was shown to the student
* @param student_answer is the first incorrect answer submitted by the student
*/
function showStudentHintRatingUX(hint, student_answer){
var hintRatingUXTemplate = $(Mustache.render($('#show_hint_rating_ux').html(), {hintText: hint}));
$('.csh_answer_text', element).append(hintRatingUXTemplate);
var hintCreationTemplate = $(Mustache.render($('#add_hint_creation').html(), {}));
$('.csh_answer_text', element).append(hintCreationTemplate);
Logger.log("crowd_hinter.hint_rating_UX", {"hint": hint, "student_answer": student_answer});
}
/**
* Show options to remove or return reported hints from/to the hint pool. Called after
* correctly answering the question, only visible to staff.
* A better method of moderating hints should probably be implemented in the future. Hints
* only can be moderated after being reported, so unreported hints will stay in the system.
* @param reportedHint is the reported hint text
*/
function showReportedModeration(reportedHint){
var reportedModerationTemplate = $(Mustache.render($('#show_reported_moderation').html(), {reportedHintText: reportedHint}));
$('.csh_reported_hints', element).append(reportedModerationTemplate);
}
/**
* Append new divisions into html for each answer the student submitted before correctly
* answering the question. showStudentHintRatingUX appends new hints into these divs.
*
* @param student_answers is the text of the student's incorrect answer
*/
function showStudentSubmissionHistory(student_answer){
var showStudentSubmissionTemplate = $(Mustache.render($('#show_student_submission').html(), {answer: student_answer}));
$('.csh_student_submission', element).append(showStudentSubmissionTemplate);
}
/**
* Set up student/staff voting on hints and contribution of new hints. The original incorrect answer and the
* the corresponding hint shown to the student is displayed. Students can upvote/downvote/report
* the hint or contribute a new hint for their incorrect answer.
*
* @param result is a dictionary of incorrect answers and hints, with the index being the hint and the value
* being the incorrect answer
*/
function setHintRatingUX(result){
if(data.isStaff){ //allow staff to see and remove/return reported hints to/from the hint pool for a problem
$('.crowdsourcehinter_block', element).attr('class', 'crowdsourcehinter_block_is_staff');
$.each(result, function(index, value) {
if(value == "Reported") {
//index represents the reported hint's text
showReportedModeration(index);
}
});
}
$.each(result, function(index, value) {
if(value != "Reported"){
showStudentSubmissionHistory(value);
student_answer = value;
hint = index;
//hints return null if no answer-specific hints exist
if(hint === "null") {
var noHintsTemplate = $(Mustache.render($('#show_no_hints').html(), {}));
$('.csh_student_answer', element).append(noHintsTemplate);
var hintCreationTemplate = $(Mustache.render($('#add_hint_creation').html(), {}));
$('.csh_student_answer', element).append(hintCreationTemplate);
Logger.log("crowd_hinter.hint_rating_UX", {"hint": "null", "student_answer": student_answer});
} else {
showStudentHintRatingUX(hint, student_answer);
}
}
});
}
/**
* Create a text input area for the student to create a new hint. This function
* is triggered by clicking the "contribute a new hint" button.
* @param createTextInputButtonHTML is the "contribute a new hint" button that was clicked
*/
function createHintContributionTextInput(){ return function(createTextInputButtonHTML){
$('.csh_student_hint_creation', element).each(function(){
$(createTextInputButtonHTML.currentTarget).show();
});
$('.csh_student_text_input', element).remove();
$('.csh_submit_new', element).remove();
$(createTextInputButtonHTML.currentTarget).hide();
student_answer = $('.csh_answer_text', element).attr('answer');
var hintTextInputTemplate = $(Mustache.render($('#hint_text_input').html(), {student_answer: student_answer}));
$('.csh_student_answer', element).append(hintTextInputTemplate); //TODO: change csh_hint_value, messes up for staff
}}
$(element).on('click', '.csh_student_hint_creation', createHintContributionTextInput($(this)));
/**
* Submit a new hint created by the student to the hint pool. Hint text is in
* the text input area created by createHintContributionTextInput. Contributed hints are specific to
* incorrect answers. Triggered by clicking the "submit hint" button.
* @param submitHintButtonHTML is the "submit hint" button clicked
*/
function submitNewHint(){ return function(submitHintButtonHTML){
//add the newly created hint to the hinter's pool of hints
if($('.csh_student_text_input', element).val().length > 0){
var studentAnswer = unescape(submitHintButtonHTML.currentTarget.attributes['answer'].value);
var newHint = unescape($('.csh_student_text_input').val());
$('.csh_submitbutton', element).show();
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'add_new_hint'),
data: JSON.stringify({"submission": newHint, "answer": studentAnswer}),
success: function() {
$('.csh_student_text_input', element).attr('style', 'display: none;');
$(submitHintButtonHTML.currentTarget).attr('style', 'display: none;');
$('.csh_hint_value', element).append("<br>Thankyou!");
Logger.log('crowd_hinter.submitNewHint', {"student_answer": studentAnswer, "new_hint_submission": newHint})
}
});
}
}}
$(element).on('click', '.csh_submit_new', submitNewHint($(this)));
/**
* Send vote data to modify a hint's rating (or mark it as reported). Triggered by
* clicking a button to upvote, downvote, or report the hint (both before and after
* the student correctly submits an answer).
* @param rateHintButtonHTML is the rate_hint button clicked (upvote/downvote/report)
*/
function rateHint(){ return function(rateHintButtonHTML){
rating = rateHintButtonHTML.currentTarget.attributes['data-rate'].value;
$('.csh_hint_text', element).attr('rating', rating);
$('.csh_hint', element).attr('rating', rating);
hint = $('.csh_hint_text', element).attr('hint_received');
student_answer = $('.csh_hint_text', element).attr('student_answer');
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'rate_hint'),
data: JSON.stringify({"student_rating": rating, "hint": hint, "student_answer": student_answer}),
success: function() {
Logger.log('crowd_hinter.rateHint', {"hint": hint, "student_answer": student_answer, "rating": rating})
$('.csh_rate_hint', element).attr('class', 'csh_rate_hint_completed');
}
});
}}
$(element).on('click', '.csh_rate_hint', rateHint($(this)));
function reportHint(){ return function(reportHintButtonHTML){
hint = $('.csh_hint_text', element).attr('hint_received');
student_answer = $('.csh_hint_text', element).attr('student_answer');
$('.csh_hint_text', element).text('This hint has been reported for review.');
$('.csh_hint', element).text('This hint has been reported for review.');
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'rate_hint'),
data: JSON.stringify({"student_rating": "report", "hint": hint, "student_answer": student_answer}),
success: function() {
Logger.log('crowd_hinter.reportHint', {"hint": hint, "student_answer": student_answer})
}
});
}}
$(element).on('click', '.csh_report_hint', reportHint($(this)));
/**
* Remove a reported hint from the reported moderation area (for staff only). Hints
* are removed from the moderation area regardless of whether they are to be permanently removed
* from the hint pool or not. Called by staffRateHint.
*/
function removeReportedHint(){
Logger.log('crowd_hinter.staffRateHint', {"hint": hint, "student_answer": student_answer, "rating": rating});
$(".csh_hint_value[value='" + hint + "']", element).remove();
}
/**
* Send staff rating data to determine whether or not a reported hint will be removed from the
* hint pool or not. Triggered by clicking a staff_rate button.
* @param staffRateHintButtonHTML is the csh_staff_rate button that was clicked
*/
function staffRateHint(){ return function(staffRateHintButtonHTML){
hint = $(staffRateHintButtonHTML.currentTarget).parent().find(".csh_hint").text();
rating = staffRateHintButtonHTML.currentTarget.attributes['data-rate'].value
student_answer = "Reported";
Logger.log('crowd_hinter.staff_rate_hint', {"hint": hint, "student_answer": student_answer, "rating": rating});
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'rate_hint'),
data: JSON.stringify({"student_rating": rating, "hint": hint, "student_answer": student_answer}),
success: removeReportedHint()
});
}}
$(element).on('click', '.csh_staff_rate', staffRateHint($(this)));
$(element).on('click', '.csh_reveal_info', function(){
if($('.csh_user_info', element).attr('style') == "display: none;"){
$('.csh_user_info', element).attr('style', "display: block;")
}else{
$('.csh_user_info', element).attr('style', "display: none;")
}
})
}
function CrowdsourceHinterStudio(runtime, element, data){
//set text area values to be what is currently in the hinter. to be replaced by above code.
$('.csh_initial_hints', element).val(data.initial);
$('.csh_generic_hints', element).val(data.generic);
$('.csh_hinting_element', element).val(data.element);
/**
* Apply settings for initial hints, generic hints, and the element for which the hinter is
* working.
*/
function apply_settings(){ return function(apply_settings_button){
var initial = unescape($('.csh_initial_hints').val());
var generic = unescape($('.csh_generic_hints').val());
var hinting_element = unescape($('.csh_hinting_element').val());
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'set_initial_settings'),
data: JSON.stringify({"initial_hints": initial, "generic_hints": generic, "element": hinting_element}),
success: function(result){
if(result.success){
$('.csh_check_success', element).text('hints successfully added to the database');
} else {
$('.csh_check_success', element).text('there was a problem adding your hints to the database. check the format on your hints.');
}
Logger.log('crowd_hinter.staff_edit_hinter', {"generic_hints": generic, "initial_hint": initial, "element": hinting_element, "successfully_set_hints": result.success});
}
});
}}
$(element).on('click', '.csh_apply_settings', apply_settings($(this)));
}
"""Setup for Crowdsource Hinter XBlock."""
import os
from setuptools import setup
def package_data(pkg, roots):
"""Generic function to find package_data.
All of the files under each of the `roots` will be declared as package
data for package `pkg`.
"""
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
setup(
name='crowdsourcehinter-xblock',
version='0.1',
description='crowdsourcehinter XBlock', # TODO: write a better description.
packages=[
'crowdsourcehinter',
],
install_requires=[
'XBlock',
],
entry_points={
'xblock.v1': [
'crowdsourcehinter = crowdsourcehinter:CrowdsourceHinter',
]
},
package_data=package_data("crowdsourcehinter", ["static", "public"]),
)
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