Commit 4422bd99 by Diana Huang

Merge branch 'master' into feature/diana/rubric-input

parents ad526e62 8a1f30bf
...@@ -12,6 +12,8 @@ admin.site.register(UserTestGroup) ...@@ -12,6 +12,8 @@ admin.site.register(UserTestGroup)
admin.site.register(CourseEnrollment) admin.site.register(CourseEnrollment)
admin.site.register(CourseEnrollmentAllowed)
admin.site.register(Registration) admin.site.register(Registration)
admin.site.register(PendingNameChange) admin.site.register(PendingNameChange)
...@@ -49,7 +49,6 @@ from django.db.models.signals import post_save ...@@ -49,7 +49,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
import comment_client as cc import comment_client as cc
from django_comment_client.models import Role
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -262,15 +261,22 @@ class CourseEnrollment(models.Model): ...@@ -262,15 +261,22 @@ class CourseEnrollment(models.Model):
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
@receiver(post_save, sender=CourseEnrollment) class CourseEnrollmentAllowed(models.Model):
def assign_default_role(sender, instance, **kwargs): """
if instance.user.is_staff: Table of users (specified by email address strings) who are allowed to enroll in a specified course.
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] The user may or may not (yet) exist. Enrollment by users listed in this table is allowed
else: even if the enrollment time window is past.
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] """
email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
instance.user.roles.add(role)
class Meta:
unique_together = (('email', 'course_id'), )
def __unicode__(self):
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
#cache_relation(User.profile) #cache_relation(User.profile)
......
...@@ -36,6 +36,7 @@ setup( ...@@ -36,6 +36,7 @@ setup(
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
] ]
} }
) )
"""
Graphical slider tool module is ungraded xmodule used by students to
understand functional dependencies.
"""
import json
import logging
from lxml import etree
from lxml import html
import xmltodict
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from pkg_resources import resource_string
log = logging.getLogger(__name__)
class GraphicalSliderToolModule(XModule):
''' Graphical-Slider-Tool Module
'''
js = {
'js': [
# 3rd party libraries used by graphic slider tool.
# TODO - where to store them - outside xmodule?
resource_string(__name__, 'js/src/graphical_slider_tool/jstat-1.0.0.min.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/state.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')
]
}
js_module_name = "GraphicalSliderTool"
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
"""
For XML file format please look at documentation. TODO - receive
information where to store XML documentation.
"""
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
def get_html(self):
""" Renders parameters to template. """
# these 3 will be used in class methods
self.html_id = self.location.html_id()
self.html_class = self.location.category
self.configuration_json = self.build_configuration_json()
params = {
'gst_html': self.substitute_controls(self.definition['render']),
'element_id': self.html_id,
'element_class': self.html_class,
'configuration_json': self.configuration_json
}
self.content = self.system.render_template(
'graphical_slider_tool.html', params)
return self.content
def substitute_controls(self, html_string):
""" Substitutes control elements (slider, textbox and plot) in
html_string with their divs. Html_string is content of <render> tag
inside <graphical_slider_tool> tag. Documentation on how information in
<render> tag is organized and processed is located in:
mitx/docs/build/html/graphical_slider_tool.html.
Args:
html_string: content of <render> tag, with controls as xml tags,
e.g. <slider var="a"/>.
Returns:
html_string with control tags replaced by proper divs
(<slider var="a"/> -> <div class="....slider" > </div>)
"""
xml = html.fromstring(html_string)
#substitute plot, if presented
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
style="{style}"></div>'
plot_el = xml.xpath('//plot')
if plot_el:
plot_el = plot_el[0]
plot_el.getparent().replace(plot_el, html.fromstring(
plot_div.format(element_class=self.html_class,
element_id=self.html_id,
style=plot_el.get('style', ""))))
#substitute sliders
slider_div = '<div class="{element_class}_slider" \
id="{element_id}_slider_{var}" \
data-var="{var}" \
style="{style}">\
</div>'
slider_els = xml.xpath('//slider')
for slider_el in slider_els:
slider_el.getparent().replace(slider_el, html.fromstring(
slider_div.format(element_class=self.html_class,
element_id=self.html_id,
var=slider_el.get('var', ""),
style=slider_el.get('style', ""))))
# substitute inputs aka textboxes
input_div = '<input class="{element_class}_input" \
id="{element_id}_input_{var}_{input_index}" \
data-var="{var}" style="{style}"/>'
input_els = xml.xpath('//textbox')
for input_index, input_el in enumerate(input_els):
input_el.getparent().replace(input_el, html.fromstring(
input_div.format(element_class=self.html_class,
element_id=self.html_id,
var=input_el.get('var', ""),
style=input_el.get('style', ""),
input_index=input_index)))
return html.tostring(xml)
def build_configuration_json(self):
"""Creates json element from xml element (with aim to transfer later
directly to javascript via hidden field in template). Steps:
1. Convert xml tree to python dict.
2. Dump dict to json.
"""
# <root> added for interface compatibility with xmltodict.parse
# class added for javascript's part purposes
return json.dumps(xmltodict.parse('<root class="' + self.html_class +
'">' + self.definition['configuration'] + '</root>'))
class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
module_class = GraphicalSliderToolModule
template_dir_name = 'graphical_slider_tool'
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the data into dictionary.
Args:
xml_object: xml from file.
Returns:
dict
"""
# check for presense of required tags in xml
expected_children_level_0 = ['render', 'configuration']
for child in expected_children_level_0:
if len(xml_object.xpath(child)) != 1:
raise ValueError("Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
expected_children_level_1 = ['functions']
for child in expected_children_level_1:
if len(xml_object.xpath('configuration')[0].xpath(child)) != 1:
raise ValueError("Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
# finished
def parse(k):
"""Assumes that xml_object has child k"""
return stringify_children(xml_object.xpath(k)[0])
return {
'render': parse('render'),
'configuration': parse('configuration')
}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
xml_object = etree.Element('graphical_slider_tool')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
xml_object.append(child_node)
for child in ['render', 'configuration']:
add_child(child)
return xml_object
// 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('ElOutput', ['logme'], function (logme) {
return ElOutput;
function ElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) {
processFuncObj(config.functions.function);
} else if ($.isArray(config.functions.function)) {
(function (c1) {
while (c1 < config.functions.function.length) {
if ($.isPlainObject(config.functions.function[c1])) {
processFuncObj(config.functions.function[c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, el, disableAutoReturn, updateOnEvent;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
((obj['@output'].toLowerCase() !== 'element') && (obj['@output'].toLowerCase() !== 'none'))
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
logme('ERROR: You specified "output" as "element", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
logme('ERROR: Function body is not defined.');
return;
}
updateOnEvent = 'slide';
if (
(obj.hasOwnProperty('@update_on') === true) &&
(typeof obj['@update_on'] === 'string') &&
((obj['@update_on'].toLowerCase() === 'slide') || (obj['@update_on'].toLowerCase() === 'change'))
) {
updateOnEvent = obj['@update_on'].toLowerCase();
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
return;
}
paramNames.pop();
if (obj['@output'].toLowerCase() !== 'none') {
el = $('#' + obj['@el_id']);
if (el.length !== 1) {
logme(
'ERROR: DOM element with ID "' + obj['@el_id'] + '" ' +
'not found. Dynamic element not created.'
);
return;
}
el.html(func.apply(window, state.getAllParameterValues()));
} else {
el = null;
func.apply(window, state.getAllParameterValues());
}
state.addDynamicEl(el, func, obj['@el_id'], updateOnEvent);
}
}
});
// 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)
// 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('GLabelElOutput', ['logme'], function (logme) {
return GLabelElOutput;
function GLabelElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) {
processFuncObj(config.functions.function);
} else if ($.isArray(config.functions.function)) {
(function (c1) {
while (c1 < config.functions.function.length) {
if ($.isPlainObject(config.functions.function[c1])) {
processFuncObj(config.functions.function[c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, disableAutoReturn;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
(obj['@output'].toLowerCase() !== 'plot_label')
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
logme('ERROR: You specified "output" as "plot_label", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
logme('ERROR: Function body is not defined.');
return;
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
return;
}
paramNames.pop();
state.plde.push({
'elId': obj['@el_id'],
'func': func
});
}
}
});
// 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)
// 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('GeneralMethods', [], function () {
if (!String.prototype.trim) {
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
String.prototype.trim = function trim(str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
}
return {
'module_name': 'GeneralMethods',
'module_status': 'OK'
};
});
// 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)
/*
* We will add a function that will be called for all GraphicalSliderTool
* xmodule module instances. It must be available globally by design of
* xmodule.
*/
window.GraphicalSliderTool = function (el) {
// All the work will be performed by the GstMain module. We will get access
// to it, and all it's dependencies, via Require JS. Currently Require JS
// is namespaced and is available via a global object RequireJS.
RequireJS.require(['GstMain'], function (GstMain) {
// The GstMain module expects the DOM ID of a Graphical Slider Tool
// element. Since we are given a <section> element which might in
// theory contain multiple graphical_slider_tool <div> elements (each
// with a unique DOM ID), we will iterate over all children, and for
// each match, we will call GstMain module.
$(el).children('.graphical_slider_tool').each(function (index, value) {
GstMain($(value).attr('id'));
});
});
};
// 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(
'GstMain',
// Even though it is not explicitly in this module, we have to specify
// 'GeneralMethods' as a dependency. It expands some of the core JS objects
// with additional useful methods that are used in other modules.
['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'ElOutput', 'GLabelElOutput', 'logme'],
function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, GLabelElOutput, logme) {
return GstMain;
function GstMain(gstId) {
var config, gstClass, state;
if ($('#' + gstId).attr('data-processed') !== 'processed') {
$('#' + gstId).attr('data-processed', 'processed');
} else {
logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
return;
}
// Get the JSON configuration, parse it, and store as an object.
try {
config = JSON.parse($('#' + gstId + '_json').html()).root;
} catch (err) {
logme('ERROR: could not parse config JSON.');
logme('$("#" + gstId + "_json").html() = ', $('#' + gstId + '_json').html());
logme('JSON.parse(...) = ', JSON.parse($('#' + gstId + '_json').html()));
logme('config = ', config);
return;
}
// Get the class name of the GST. All elements are assigned a class
// name that is based on the class name of the GST. For example, inputs
// are assigned a class name '{GST class name}_input'.
if (typeof config['@class'] !== 'string') {
logme('ERROR: Could not get the class name of GST.');
logme('config["@class"] = ', config['@class']);
return;
}
gstClass = config['@class'];
// Parse the configuration settings for parameters, and store them in a
// state object.
state = State(gstId, config);
// It is possible that something goes wrong while extracting parameters
// from the JSON config object. In this case, we will not continue.
if (state === undefined) {
logme('ERROR: The state object was not initialized properly.');
return;
}
// Create the sliders and the text inputs, attaching them to
// appropriate parameters.
Sliders(gstId, state);
Inputs(gstId, gstClass, state);
// Configure functions that output to an element instead of the graph.
ElOutput(config, state);
// Configure functions that output to an element instead of the graph
// label.
GLabelElOutput(config, state);
// Configure and display the graph. Attach event for the graph to be
// updated on any change of a slider or a text input.
Graph(gstId, config, state);
}
});
// 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)
// 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('Inputs', ['logme'], function (logme) {
return Inputs;
function Inputs(gstId, gstClass, state) {
var c1, paramName, allParamNames;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
$('#' + gstId).find('.' + gstClass + '_input').each(function (index, value) {
var inputDiv, paramName;
paramName = allParamNames[c1];
inputDiv = $(value);
if (paramName === inputDiv.data('var')) {
createInput(inputDiv, paramName);
}
});
}
return;
function createInput(inputDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Bind a function to the 'change' event. Whenever the user changes
// the value of this text input, and presses 'enter' (or clicks
// somewhere else on the page), this event will be triggered, and
// our callback will be called.
inputDiv.bind('change', inputOnChange);
inputDiv.val(paramObj.value);
// Lets style the input element nicely. We will use the button()
// widget for this since there is no native widget for the text
// input.
inputDiv.button().css({
'font': 'inherit',
'color': 'inherit',
'text-align': 'left',
'outline': 'none',
'cursor': 'text',
'height': '15px'
});
// Tell the parameter object from state that we are attaching a
// text input to it. Next time the parameter will be updated with
// a new value, tis input will also be updated.
paramObj.inputDivs.push(inputDiv);
return;
// Update the 'state' - i.e. set the value of the parameter this
// input is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// changes the value in the input. Note that he has to either press
// 'Enter', or click somewhere else on the page in order for the
// 'change' event to be tiggered.
function inputOnChange(event) {
var inputDiv;
inputDiv = $(this);
state.setParameterValue(paramName, inputDiv.val(), inputDiv);
}
}
}
});
// 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)
// 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)
// 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('Sliders', ['logme'], function (logme) {
return Sliders;
function Sliders(gstId, state) {
var c1, paramName, allParamNames, sliderDiv;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
paramName = allParamNames[c1];
sliderDiv = $('#' + gstId + '_slider_' + paramName);
if (sliderDiv.length === 1) {
createSlider(sliderDiv, paramName);
} else if (sliderDiv.length > 1) {
logme('ERROR: Found more than one slider for the parameter "' + paramName + '".');
logme('sliderDiv.length = ', sliderDiv.length);
} else {
logme('MESSAGE: Did not find a slider for the parameter "' + paramName + '".');
}
}
function createSlider(sliderDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Create a jQuery UI slider from the slider DIV. We will set
// starting parameters, and will also attach a handler to update
// the 'state' on the 'slide' event.
sliderDiv.slider({
'min': paramObj.min,
'max': paramObj.max,
'value': paramObj.value,
'step': paramObj.step
});
// Tell the parameter object stored in state that we have a slider
// that is attached to it. Next time when the parameter changes, it
// will also update the value of this slider.
paramObj.sliderDiv = sliderDiv;
// Atach callbacks to update the slider's parameter.
paramObj.sliderDiv.on('slide', sliderOnSlide);
paramObj.sliderDiv.on('slidechange', sliderOnChange);
return;
// Update the 'state' - i.e. set the value of the parameter this
// slider is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// drags the slider handle and releases it.
function sliderOnSlide(event, ui) {
// Last parameter passed to setParameterValue() will be 'true'
// so that the function knows we are a slider, and it can
// change the our value back in the case when the new value is
// invalid for some reason.
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'slide') === undefined) {
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
function sliderOnChange(event, ui) {
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'change') === undefined) {
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
}
}
});
// 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)
...@@ -39,9 +39,12 @@ def strip_filenames(descriptor): ...@@ -39,9 +39,12 @@ def strip_filenames(descriptor):
class RoundTripTestCase(unittest.TestCase): class RoundTripTestCase(unittest.TestCase):
'''Check that our test courses roundtrip properly''' ''' Check that our test courses roundtrip properly.
Same course imported , than exported, then imported again.
And we compare original import with second import (after export).
Thus we make sure that export and import work properly.
'''
def check_export_roundtrip(self, data_dir, course_dir): def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(mkdtemp()) root_dir = path(mkdtemp())
print "Copying test course to temp dir {0}".format(root_dir) print "Copying test course to temp dir {0}".format(root_dir)
...@@ -117,3 +120,7 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -117,3 +120,7 @@ class RoundTripTestCase(unittest.TestCase):
def test_selfassessment_roundtrip(self): def test_selfassessment_roundtrip(self):
#Test selfassessment xmodule to see if it exports correctly #Test selfassessment xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"self_assessment") self.check_export_roundtrip(DATA_DIR,"self_assessment")
def test_graphicslidertool_roundtrip(self):
#Test graphicslidertool xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool")
...@@ -352,3 +352,19 @@ class ImportTestCase(unittest.TestCase): ...@@ -352,3 +352,19 @@ class ImportTestCase(unittest.TestCase):
sa_sample = modulestore.get_instance(sa_id, location) sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag #10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
self.assertEqual(sa_sample.metadata['attempts'], '10') self.assertEqual(sa_sample.metadata['attempts'], '10')
def test_graphicslidertool_import(self):
'''
Check to see if definition_from_xml in gst_module.py
works properly. Pulls data from the graphic_slider_tool directory
in the test data directory.
'''
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool'])
sa_id = "edX/gst_test/2012_Fall"
location = Location(["i4x", "edX", "gst_test", "graphical_slider_tool", "sample_gst"])
gst_sample = modulestore.get_instance(sa_id, location)
render_string_from_sample_gst_xml = """
<slider var="a" style="width:400px;float:left;"/>\
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
self.assertEqual(gst_sample.definition['render'], render_string_from_sample_gst_xml)
This is a very very simple course, useful for debugging graphical slider tool
code.
roots/2012_Fall.xml
\ No newline at end of file
<course>
<chapter url_name="Overview">
<graphical_slider_tool url_name="sample_gst"/>
</chapter>
</course>
<graphical_slider_tool>
<render>
<slider var='a' style="width:400px;float:left;"/><plot style="margin-top:15px;margin-bottom:15px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
</parameters>
<functions>
<function color="red">return Math.sqrt(a * a - x * x);</function>
<function color="red">return -Math.sqrt(a * a - x * x);</function>
</functions>
<plot>
<xrange>
<!-- dynamic range -->
<min>
return -a;
</min>
<max>
return a;
</max>
</xrange>
<num_points>1000</num_points>
<xticks>-30, 6, 30</xticks>
<yticks>-30, 6, 30</yticks>
</plot>
</configuration>
</graphical_slider_tool>
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "GST Test",
"graded": "false"
},
"chapter/Overview": {
"display_name": "Overview"
},
"graphical_slider_tool/sample_gst": {
"display_name": "Sample GST",
},
}
<course org="edX" course="gst_test" url_name="2012_Fall"/>
roots/2012_Fall.xml
\ No newline at end of file
<course org="edX" course="sa_test" url_name="2012_Fall"/>
...@@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively. ...@@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively.
Note that the default weight of a problem **is not 1.** The default weight of a Note that the default weight of a problem **is not 1.** The default weight of a
problem is the module's max_grade. problem is the module's max_grade.
If weighting is set, each problem is worth the number of points assigned, regardless of the number of responses it contains.
Consider a Homework section that contains two problems.
<problem display_name=”Problem 1”>
<numericalresponse> ... </numericalreponse>
</problem>
and
<problem display_name=”Problem 2”>
<numericalresponse> ... </numericalreponse>
<numericalresponse> ... </numericalreponse>
<numericalresponse> ... </numericalreponse>
</problem>
Without weighting, Problem 1 is worth 25% of the assignment, and Problem 2 is worth 75% of the assignment.
Weighting for the problems can be set in the policy.json file.
"problem/problem1": {
"weight": 2
},
"problem/problem2": {
"weight": 2
},
With the above weighting, Problems 1 and 2 are each worth 50% of the assignment.
Please note: When problems have weight, the point value is automatically included in the display name *except* when “weight”: 1.When “weight”: 1, no visual change occurs in the display name, leaving the point value open to interpretation to the student.
## Section Weighting ## Section Weighting
Once each section has a percentage score, we must total those sections into a Once each section has a percentage score, we must total those sections into a
......
Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers".
1. Definitions
An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages.
"Stellar" is the MIT on-campus gradebook system.
2. Setup
The remote gradebook xserver should be specified in the lms.envs configuration using
MITX_FEATURES[REMOTE_GRADEBOOK_URL]
Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg:
"remote_gradebook": {
"name" : "STELLAR:/project/mitxdemosite",
"section" : "r01"
},
3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields:
- submit: get-assignments, get-membership, post-grades, or get-sections
- gradebook: name of gradebook
- user: username of staff person initiating the request (for logging)
- section: (optional) name of section
The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard.
The data is a list of dicts (associative arrays). Each dict should be key:value.
## For submit=post-grades:
A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment).
## For submit=get-assignments
data keys = "AssignmentName"
## For submit=get-membership
data keys = "email", "name", "section"
## For submit=get-sections
data keys = "SectionName"
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Bar graph example.</h2>
<p>We can request the API to plot us a bar graph.</p>
<div style="clear:both">
<p style="width:60px;float:left;">a</p>
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
<br /><br /><br />
<p style="width:60px;float:left;">b</p>
<slider var='b' style="width:400px;float:left;"/>
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
</div>
<plot style="clear:left;"/>
</render>
<configuration>
<parameters>
<param var="a" min="-100" max="100" step="5" initial="25" />
<param var="b" min="-100" max="100" step="5" initial="50" />
</parameters>
<functions>
<function bar="true" color="blue" label="Men">
<![CDATA[if (((x>0.9) && (x<1.1)) || ((x>4.9) && (x<5.1))) { return Math.sin(a * 0.01 * Math.PI + 2.952 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="red" label="Women">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos(b * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="green" label="Other 1">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b - 10 * a) * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="yellow" label="Other 2">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b + 7 * a) * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
</functions>
<plot>
<xrange><min>1</min><max>5</max></xrange>
<num_points>5</num_points>
<xticks>0, 0.5, 6</xticks>
<yticks>-1.5, 0.1, 1.5</yticks>
<xticks_names>
<![CDATA[
{
"1.5": "Single", "4.5": "Married"
}
]]>
</xticks_names>
<yticks_names>
<![CDATA[
{
"-1.0": "-100%", "-0.5": "-50%", "0.0": "0%", "0.5": "50%", "1.0": "100%"
}
]]>
</yticks_names>
<bar_width>0.4</bar_width>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>
<vertical>
<graphical_slider_tool>
<render>
<h1>Graphic slider tool: Dynamic labels.</h1>
<p>There are two kinds of dynamic lables.
1) Dynamic changing values in graph legends.
2) Dynamic labels, which coordinates depend on parameters </p>
<p>a: <slider var="a"/></p>
<br/>
<p>b: <slider var="b"/></p>
<br/><br/>
<plot style="width:400px; height:400px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="-10" max="10" step="1" initial="0" />
<param var="b" min="0" max="10" step="0.5" initial="5" />
</parameters>
<functions>
<function label="Value of a is: dyn_val_1">a * x + b</function>
<!-- dynamic values in legend -->
<function output="plot_label" el_id="dyn_val_1">a</function>
</functions>
<plot>
<xrange><min>0</min><max>30</max></xrange>
<num_points>10</num_points>
<xticks>0, 6, 30</xticks>
<yticks>-9, 1, 9</yticks>
<!-- custom labels with coordinates as any function of parameter -->
<moving_label text="Dynam_lbl 1" weight="bold">
<![CDATA[ {'x': 10, 'y': a};]]>
</moving_label>
<moving_label text="Dynam lbl 2" weight="bold">
<![CDATA[ {'x': -6, 'y': b};]]>
</moving_label>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>
\ No newline at end of file
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Dynamic range and implicit functions.</h2>
<p>You can make x range (not ticks of x axis) of functions to depend on
parameter value. This can be useful when function domain depends
on parameter.</p>
<p>Also implicit functons like circle can be plotted as 2 separate
functions of same color.</p>
<div style="height:50px;">
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="float:left;width:60px;margin-left:15px;"/>
</div>
<plot style="margin-top:15px;margin-bottom:15px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
</parameters>
<functions>
<function color="red">Math.sqrt(a * a - x * x)</function>
<function color="red">-Math.sqrt(a * a - x * x)</function>
</functions>
<plot>
<xrange>
<!-- dynamic range -->
<min>-a</min>
<max>a</max>
</xrange>
<num_points>1000</num_points>
<xticks>-30, 6, 30</xticks>
<yticks>-30, 6, 30</yticks>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Output to DOM element.</h2>
<p>a + b = <span id="answer_span_1"></span></p>
<div style="clear:both">
<p style="float:left;margin-right:10px;">a</p>
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
</div>
<div style="clear:both">
<p style="float:left;margin-right:10px;">b</p>
<slider var='b' style="width:400px;float:left;"/>
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
</div>
<br/><br/><br/>
<plot/>
</render>
<configuration>
<parameters>
<param var="a" min="-10.0" max="10.0" step="0.1" initial="0" />
<param var="b" min="-10.0" max="10.0" step="0.1" initial="0" />
</parameters>
<functions>
<function output="element" el_id="answer_span_1">
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
return add(a, b, 5);
</function>
</functions>
</configuration>
</graphical_slider_tool>
</vertical>
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: full example.</h2>
<p>
A simple equation
\(
y_1 = 10 \times b \times \frac{sin(a \times x) \times sin(b \times x)}{cos(b \times x) + 10}
\)
can be plotted.
</p>
<!-- make text and input or slider at the same line -->
<div>
<p style="float:left;"> Currently \(a\) is</p>
<!-- readonly input for a -->
<span id="a_readonly" style="width:50px; float:left; margin-left:10px;"/>
</div>
<!-- clear:left will make next text to begin from new line -->
<p style="clear:left"> This one
\(
y_2 = sin(a \times x)
\)
will be overlayed on top.
</p>
<div>
<p style="float:left;">Currently \(b\) is </p>
<textbox var="b" style="width:50px; float:left; margin-left:10px;"/>
</div>
<div style="clear:left;">
<p style="float:left;">To change \(a\) use:</p>
<slider var="a" style="width:400px;float:left;margin-left:10px;"/>
</div>
<div style="clear:left;">
<p style="float:left;">To change \(b\) use:</p>
<slider var="b" style="width:400px;float:left;margin-left:10px;"/>
</div>
<plot style='clear:left;width:600px;padding-top:15px;padding-bottom:20px;'/>
<div style="clear:left;height:50px;">
<p style="float:left;">Second input for b:</p>
<!-- editable input for b -->
<textbox var="b" style="color:red;width:60px;float:left;margin-left:10px;"/>
</div >
</render>
<configuration>
<parameters>
<param var="a" min="90" max="120" step="10" initial="100" />
<param var="b" min="120" max="200" step="2.3" initial="120" />
</parameters>
<functions>
<function color="#0000FF" line="false" dot="true" label="\(y_1\)">
return 10.0 * b * Math.sin(a * x) * Math.sin(b * x) / (Math.cos(b * x) + 10);
</function>
<function color="red" line="true" dot="false" label="\(y_2\)">
<!-- works w/o return, if function is single line -->
Math.sin(a * x);
</function>
<function color="#FFFF00" line="false" dot="false" label="unknown">
function helperFunc(c1) {
return c1 * c1 - a;
}
return helperFunc(x + 10 * a * b) + Math.sin(a - x);
</function>
<function output="element" el_id="a_readonly">a</function>
</functions>
<plot>
<xrange>
<min>return 0;</min>
<!-- works w/o return -->
<max>30</max>
</xrange>
<num_points>120</num_points>
<xticks>0, 3, 30</xticks>
<yticks>-1.5, 1.5, 13.5</yticks>
<xunits>\(cm\)</xunits>
<yunits>\(m\)</yunits>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>
...@@ -14,7 +14,7 @@ Contents: ...@@ -14,7 +14,7 @@ Contents:
overview.rst overview.rst
common-lib.rst common-lib.rst
djangoapps.rst djangoapps.rst
xml_formats.rst
Indices and tables Indices and tables
================== ==================
......
XML formats of Inputtypes and Xmodule
=====================================
Contents:
.. toctree::
:maxdepth: 2
graphical_slider_tool.rst
\ No newline at end of file
...@@ -28,6 +28,12 @@ export PYTHONIOENCODING=UTF-8 ...@@ -28,6 +28,12 @@ export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master} GIT_BRANCH=${GIT_BRANCH/HEAD/master}
# Temporary workaround for pip/numpy bug. (Jenkin's is unable to pip install numpy successfully, scipy fails to install afterwards.
# We tried pip 1.1, 1.2, all sorts of varieties but it's apparently a pip bug of some kind.
wget -O /tmp/numpy.tar.gz http://pypi.python.org/packages/source/n/numpy/numpy-1.6.2.tar.gz#md5=95ed6c9dcc94af1fc1642ea2a33c1bba
tar -zxvf /tmp/numpy.tar.gz -C /tmp/
python /tmp/numpy-1.6.2/setup.py install
pip install -q -r pre-requirements.txt pip install -q -r pre-requirements.txt
pip install -q -r test-requirements.txt pip install -q -r test-requirements.txt
yes w | pip install -q -r requirements.txt yes w | pip install -q -r requirements.txt
......
...@@ -13,6 +13,8 @@ from xmodule.modulestore import Location ...@@ -13,6 +13,8 @@ from xmodule.modulestore import Location
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed
DEBUG_ACCESS = False DEBUG_ACCESS = False
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -124,6 +126,11 @@ def _has_access_course_desc(user, course, action): ...@@ -124,6 +126,11 @@ def _has_access_course_desc(user, course, action):
debug("Allow: in enrollment period") debug("Allow: in enrollment period")
return True return True
# if user is in CourseEnrollmentAllowed with right course_id then can also enroll
if user is not None and CourseEnrollmentAllowed:
if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id):
return True
# otherwise, need staff access # otherwise, need staff access
return _has_staff_access_to_descriptor(user, course) return _has_staff_access_to_descriptor(user, course)
...@@ -159,13 +166,19 @@ def _has_access_course_desc(user, course, action): ...@@ -159,13 +166,19 @@ def _has_access_course_desc(user, course, action):
return _dispatch(checkers, action, user, course) return _dispatch(checkers, action, user, course)
def _get_access_group_name_course_desc(course, action): def _get_access_group_name_course_desc(course, action):
''' '''
Return name of group which gives staff access to course. Only understands action = 'staff' Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor'
''' '''
if not action=='staff': if action=='staff':
return []
return _course_staff_group_name(course.location) return _course_staff_group_name(course.location)
elif action=='instructor':
return _course_instructor_group_name(course.location)
return []
def _has_access_error_desc(user, descriptor, action): def _has_access_error_desc(user, descriptor, action):
""" """
......
...@@ -7,3 +7,8 @@ from django.contrib import admin ...@@ -7,3 +7,8 @@ from django.contrib import admin
from django.contrib.auth.models import User from django.contrib.auth.models import User
admin.site.register(StudentModule) admin.site.register(StudentModule)
admin.site.register(OfflineComputedGrade)
admin.site.register(OfflineComputedGradeLog)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'OfflineComputedGrade'
db.create_table('courseware_offlinecomputedgrade', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
('gradeset', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
))
db.send_create_signal('courseware', ['OfflineComputedGrade'])
# Adding unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id']
db.create_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id'])
# Adding model 'OfflineComputedGradeLog'
db.create_table('courseware_offlinecomputedgradelog', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('seconds', self.gf('django.db.models.fields.IntegerField')(default=0)),
('nstudents', self.gf('django.db.models.fields.IntegerField')(default=0)),
))
db.send_create_signal('courseware', ['OfflineComputedGradeLog'])
def backwards(self, orm):
# Removing unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id']
db.delete_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id'])
# Deleting model 'OfflineComputedGrade'
db.delete_table('courseware_offlinecomputedgrade')
# Deleting model 'OfflineComputedGradeLog'
db.delete_table('courseware_offlinecomputedgradelog')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'courseware.offlinecomputedgrade': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'courseware.offlinecomputedgradelog': {
'Meta': {'object_name': 'OfflineComputedGradeLog'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'courseware.studentmodule': {
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['courseware']
\ No newline at end of file
...@@ -177,3 +177,40 @@ class StudentModuleCache(object): ...@@ -177,3 +177,40 @@ class StudentModuleCache(object):
def append(self, student_module): def append(self, student_module):
self.cache.append(student_module) self.cache.append(student_module)
class OfflineComputedGrade(models.Model):
"""
Table of grades computed offline for a given user and course.
"""
user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
gradeset = models.TextField(null=True, blank=True) # grades, stored as JSON
class Meta:
unique_together = (('user', 'course_id'), )
def __unicode__(self):
return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset)
class OfflineComputedGradeLog(models.Model):
"""
Log of when offline grades are computed.
Use this to be able to show instructor when the last computed grades were done.
"""
class Meta:
ordering = ["-created"]
get_latest_by = "created"
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
seconds = models.IntegerField(default=0) # seconds elapsed for computation
nstudents = models.IntegerField(default=0)
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)
...@@ -2,6 +2,10 @@ import logging ...@@ -2,6 +2,10 @@ import logging
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
from student.models import CourseEnrollment
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
...@@ -45,3 +49,14 @@ class Permission(models.Model): ...@@ -45,3 +49,14 @@ class Permission(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@receiver(post_save, sender=CourseEnrollment)
def assign_default_role(sender, instance, **kwargs):
if instance.user.is_staff:
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
else:
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role)
#!/usr/bin/python
#
# django management command: dump grades to csv files
# for use by batch processes
import os, sys, string
import datetime
import json
#import student.models
from instructor.offline_gradecalc import *
from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Compute grades for all students in a course, and store result in DB.\n"
help += "Usage: compute_grades course_id_or_dir \n"
help += " course_id_or_dir: either course_id or course_dir\n"
help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section'
def handle(self, *args, **options):
print "args = ", args
if len(args)>0:
course_id = args[0]
else:
print self.help
return
try:
course = get_course_by_id(course_id)
except Exception as err:
if course_id in modulestore().courses:
course = modulestore().courses[course_id]
else:
print "-----------------------------------------------------------------------------"
print "Sorry, cannot find course %s" % course_id
print "Please provide a course ID or course data directory name, eg content-mit-801rq"
return
print "-----------------------------------------------------------------------------"
print "Computing grades for %s" % (course.id)
offline_grade_calculation(course.id)
# ======== Offline calculation of grades =============================================================================
#
# Computing grades of a large number of students can take a long time. These routines allow grades to
# be computed offline, by a batch process (eg cronjob).
#
# The grades are stored in the OfflineComputedGrade table of the courseware model.
import json
import logging
import time
import courseware.models
from collections import namedtuple
from json import JSONEncoder
from courseware import grades, models
from courseware.courses import get_course_by_id
from django.contrib.auth.models import User, Group
class MyEncoder(JSONEncoder):
def _iterencode(self, obj, markers=None):
if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
gen = self._iterencode_dict(obj._asdict(), markers)
else:
gen = JSONEncoder._iterencode(self, obj, markers)
for chunk in gen:
yield chunk
def offline_grade_calculation(course_id):
'''
Compute grades for all students for a specified course, and save results to the DB.
'''
tstart = time.time()
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username')
enc = MyEncoder()
class DummyRequest(object):
META = {}
def __init__(self):
return
def get_host(self):
return 'edx.mit.edu'
def is_secure(self):
return False
request = DummyRequest()
print "%d enrolled students" % len(enrolled_students)
course = get_course_by_id(course_id)
for student in enrolled_students:
gradeset = grades.grade(student, request, course, keep_raw_scores=True)
gs = enc.encode(gradeset)
ocg, created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_id)
ocg.gradeset = gs
ocg.save()
print "%s done" % student # print statement used because this is run by a management command
tend = time.time()
dt = tend - tstart
ocgl = models.OfflineComputedGradeLog(course_id=course_id, seconds=dt, nstudents=len(enrolled_students))
ocgl.save()
print ocgl
print "All Done!"
def offline_grades_available(course_id):
'''
Returns False if no offline grades available for specified course.
Otherwise returns latest log field entry about the available pre-computed grades.
'''
ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_id)
if not ocgl:
return False
return ocgl.latest('created')
def student_grades(student, request, course, keep_raw_scores=False, use_offline=False):
'''
This is the main interface to get grades. It has the same parameters as grades.grade, as well
as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB.
'''
if not use_offline:
return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores)
try:
ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id)
except models.OfflineComputedGrade.DoesNotExist:
return dict(raw_scores=[], section_breakdown=[],
msg='Error: no offline gradeset available for %s, %s' % (student, course.id))
return json.loads(ocg.gradeset)
...@@ -430,6 +430,7 @@ courseware_only_js += [ ...@@ -430,6 +430,7 @@ courseware_only_js += [
main_vendor_js = [ main_vendor_js = [
'js/vendor/RequireJS.js', 'js/vendor/RequireJS.js',
'js/vendor/json2.js', 'js/vendor/json2.js',
'js/vendor/RequireJS.js',
'js/vendor/jquery.min.js', 'js/vendor/jquery.min.js',
'js/vendor/jquery-ui.min.js', 'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.cookie.js', 'js/vendor/jquery.cookie.js',
......
...@@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = { ...@@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = {
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
################################# mitx revision string #####################
MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip()
################################# Staff grading config ##################### ################################# Staff grading config #####################
STAFF_GRADING_INTERFACE = { STAFF_GRADING_INTERFACE = {
......
...@@ -57,10 +57,13 @@ function goto( mode) ...@@ -57,10 +57,13 @@ function goto( mode)
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> | <a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
%endif %endif
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> | <a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> ] <a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a>
]
</h2> </h2>
<div style="text-align:right" id="djangopid">${djangopid}</div> <div style="text-align:right"><span id="djangopid">${djangopid}</span>
| <span id="mitxver">${mitx_version}</span></div>
<form name="idashform" method="POST"> <form name="idashform" method="POST">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
...@@ -68,6 +71,12 @@ function goto( mode) ...@@ -68,6 +71,12 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Grades'): %if modeflag.get('Grades'):
%if offline_grade_log:
<p><font color='orange'>Pre-computed grades ${offline_grade_log} available: Use?
<input type='checkbox' name='use_offline_grades' value='yes'></font> </p>
%endif
<p> <p>
<a href="${reverse('gradebook', kwargs=dict(course_id=course.id))}">Gradebook</a> <a href="${reverse('gradebook', kwargs=dict(course_id=course.id))}">Gradebook</a>
</p> </p>
...@@ -93,6 +102,42 @@ function goto( mode) ...@@ -93,6 +102,42 @@ function goto( mode)
<p> <p>
<input type="submit" name="action" value="Download CSV of answer distributions"> <input type="submit" name="action" value="Download CSV of answer distributions">
</p> </p>
<hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
<%
rg = course.metadata.get('remote_gradebook',{})
%>
<h3>Export grades to remote gradebook</h3>
<p>The assignments defined for this course should match the ones
stored in the gradebook, for this to work properly!</p>
<ul>
<li>Gradebook name: <font color="green">${rg.get('name','None defined!')}</font>
<br/>
<br/>
<input type="submit" name="action" value="List assignments available in remote gradebook">
<input type="submit" name="action" value="List enrolled students matching remote gradebook">
<br/>
<br/>
</li>
<li><input type="submit" name="action" value="List assignments available for this course">
<br/>
<br/>
</li>
<li>Assignment name: <input type="text" name="assignment_name" size=40 >
<br/>
<br/>
<input type="submit" name="action" value="Display grades for assignment">
<input type="submit" name="action" value="Export grades for assignment to remote gradebook">
<input type="submit" name="action" value="Export CSV file of grades for assignment">
</li>
</ul>
%endif
%endif %endif
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
...@@ -128,6 +173,16 @@ function goto( mode) ...@@ -128,6 +173,16 @@ function goto( mode)
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
%endif %endif
%if admin_access:
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List course instructors">
<p>
<input type="text" name="instructor"> <input type="submit" name="action" value="Remove instructor">
<input type="submit" name="action" value="Add instructor">
<hr width="40%" style="align:left">
%endif
%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access: %if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
<p> <p>
<input type="submit" name="action" value="Reload course from XML files"> <input type="submit" name="action" value="Reload course from XML files">
...@@ -163,10 +218,52 @@ function goto( mode) ...@@ -163,10 +218,52 @@ function goto( mode)
%endif %endif
%endif %endif
##-----------------------------------------------------------------------------
%if modeflag.get('Enrollment'):
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List enrolled students">
<input type="submit" name="action" value="List students who may enroll but may not have yet signed up">
<p>
Student Email: <input type="text" name="enstudent"> <input type="submit" name="action" value="Un-enroll student">
<input type="submit" name="action" value="Enroll student">
<input type="submit" name="action" value="Un-enroll ALL students">
<hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
<%
rg = course.metadata.get('remote_gradebook',{})
%>
<p>Pull enrollment from remote gradebook</p>
<ul>
<li>Gradebook name: <font color="green">${rg.get('name','None defined!')}</font>
<li>Section: <input type="text" name="gradebook_section" size=40 value="${rg.get('section','')}"></li>
</ul>
<input type="submit" name="action" value="List sections available in remote gradebook">
<input type="submit" name="action" value="List students in section in remote gradebook">
<input type="submit" name="action" value="Overload enrollment list using remote gradebook">
<input type="submit" name="action" value="Merge enrollment list with remote gradebook">
<hr width="40%" style="align:left">
%endif
<p>Add students: enter emails, separated by returns or commas;</p>
<textarea rows="6" cols="70" name="enroll_multiple"></textarea>
<input type="submit" name="action" value="Enroll multiple students">
%endif
##-----------------------------------------------------------------------------
</form> </form>
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Psychometrics') is None: ##-----------------------------------------------------------------------------
%if datatable and modeflag.get('Psychometrics') is None:
<br/> <br/>
<br/> <br/>
......
<div align="center" id="${element_id}" class="${element_class}">
<!-- hidden field to read configuration json from -->
<div class="${element_class}" id="${element_id}_json" style="display: none;">
${configuration_json}
</div>
<!-- main xml with marked places for sliders, numbers, and plot -->
${gst_html}
</div>
...@@ -56,3 +56,4 @@ dogstatsd-python==0.2.1 ...@@ -56,3 +56,4 @@ dogstatsd-python==0.2.1
sphinx==1.1.3 sphinx==1.1.3
Shapely==1.2.16 Shapely==1.2.16
ipython==0.13.1 ipython==0.13.1
xmltodict==0.4.1
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