Merge branch 'master' into kimth/fix-dynamath-6002x
@@ -53,7 +53,7 @@ def index(request):
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
return render_to_response('index.html', {
|
||||
'courses': [(course.metadata['display_name'],
|
||||
'courses': [(course.metadata.get('display_name'),
|
||||
reverse('course_index', args=[
|
||||
course.location.org,
|
||||
course.location.course,
|
||||
|
||||
@@ -14,9 +14,11 @@ $yellow: #fff8af;
|
||||
$cream: #F6EFD4;
|
||||
$border-color: #ddd;
|
||||
|
||||
|
||||
// edX colors
|
||||
$blue: rgb(29,157,217);
|
||||
$pink: rgb(182,37,104);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
@mixin hide-text {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -330,11 +330,6 @@ section.cal {
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
width: flex-grid(5) + flex-gutter();
|
||||
|
||||
+ section.main-content {
|
||||
width: flex-grid(7);
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
|
||||
@@ -5,4 +5,8 @@ django admin pages for courseware model
|
||||
from external_auth.models import *
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(ExternalAuthMap)
|
||||
class ExternalAuthMapAdmin(admin.ModelAdmin):
|
||||
search_fields = ['external_id','user__username']
|
||||
date_hierarchy = 'dtcreated'
|
||||
|
||||
admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin)
|
||||
|
||||
8
common/djangoapps/track/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
'''
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from track.models import *
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(TrackingLog)
|
||||
@@ -11,13 +11,11 @@ importAll("xproblem");
|
||||
|
||||
generatorModulePath = process.argv[2];
|
||||
dependencies = JSON.parse(process.argv[3]);
|
||||
seed = process.argv[4];
|
||||
seed = JSON.parse(process.argv[4]);
|
||||
params = JSON.parse(process.argv[5]);
|
||||
|
||||
if(seed==null){
|
||||
seed = 4;
|
||||
}else{
|
||||
seed = parseInt(seed);
|
||||
}
|
||||
|
||||
for(var i = 0; i < dependencies.length; i++){
|
||||
|
||||
@@ -408,7 +408,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
output = self.call_node([generator_file,
|
||||
self.generator,
|
||||
json.dumps(self.generator_dependencies),
|
||||
json.dumps(str(self.system.seed)),
|
||||
json.dumps(str(self.context['the_lcp'].seed)),
|
||||
json.dumps(self.params)]).strip()
|
||||
|
||||
return json.loads(output)
|
||||
|
||||
@@ -507,8 +507,12 @@ class CapaModule(XModule):
|
||||
# 'success' will always be incorrect
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ nav.sequence-nav {
|
||||
|
||||
.sequence-list-wrapper {
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
z-index: 99;
|
||||
border: 1px solid #ccc;
|
||||
height: 44px;
|
||||
margin: 0 30px;
|
||||
|
||||
@@ -2023,7 +2023,16 @@ function add_schematic_handler(other_onload) {
|
||||
update_schematics();
|
||||
}
|
||||
}
|
||||
window.onload = add_schematic_handler(window.onload);
|
||||
/*
|
||||
* THK: Attaching update_schematic to window.onload is rather presumptuous...
|
||||
* The function is called for EVERY page load, whether in courseware or in
|
||||
* course info, in 6.002x or the public health course. It is also redundant
|
||||
* because courseware includes an explicit call to update_schematic after
|
||||
* each ajax exchange. In this case, calling update_schematic twice appears
|
||||
* to contribute to a bug in Firefox that does not render the schematic
|
||||
* properly depending on timing.
|
||||
*/
|
||||
//window.onload = add_schematic_handler(window.onload);
|
||||
|
||||
// ask each schematic input widget to update its value field for submission
|
||||
function prepare_schematics() {
|
||||
|
||||
@@ -75,7 +75,7 @@ class SequenceModule(XModule):
|
||||
contents = []
|
||||
for child in self.get_display_items():
|
||||
progress = child.get_progress()
|
||||
contents.append({
|
||||
childinfo = {
|
||||
'content': child.get_html(),
|
||||
'title': "\n".join(
|
||||
grand_child.display_name.strip()
|
||||
@@ -85,7 +85,10 @@ class SequenceModule(XModule):
|
||||
'progress_status': Progress.to_js_status_str(progress),
|
||||
'progress_detail': Progress.to_js_detail_str(progress),
|
||||
'type': child.get_icon_class(),
|
||||
})
|
||||
}
|
||||
if childinfo['title']=='':
|
||||
childinfo['title'] = child.metadata.get('display_name','')
|
||||
contents.append(childinfo)
|
||||
|
||||
params = {'items': contents,
|
||||
'element_id': self.location.html_id(),
|
||||
|
||||
412
common/static/js/vendor/flot/jquery.flot.axislabels.js
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
Axis Labels Plugin for flot.
|
||||
http://github.com/markrcote/flot-axislabels
|
||||
|
||||
Original code is Copyright (c) 2010 Xuan Luo.
|
||||
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
|
||||
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
|
||||
|
||||
Improvements by Mark Cote.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
(function ($) {
|
||||
var options = { };
|
||||
|
||||
function canvasSupported() {
|
||||
return !!document.createElement('canvas').getContext;
|
||||
}
|
||||
|
||||
function canvasTextSupported() {
|
||||
if (!canvasSupported()) {
|
||||
return false;
|
||||
}
|
||||
var dummy_canvas = document.createElement('canvas');
|
||||
var context = dummy_canvas.getContext('2d');
|
||||
return typeof context.fillText == 'function';
|
||||
}
|
||||
|
||||
function css3TransitionSupported() {
|
||||
var div = document.createElement('div');
|
||||
return typeof div.style.MozTransition != 'undefined' // Gecko
|
||||
|| typeof div.style.OTransition != 'undefined' // Opera
|
||||
|| typeof div.style.webkitTransition != 'undefined' // WebKit
|
||||
|| typeof div.style.transition != 'undefined';
|
||||
}
|
||||
|
||||
|
||||
function AxisLabel(axisName, position, padding, plot, opts) {
|
||||
this.axisName = axisName;
|
||||
this.position = position;
|
||||
this.padding = padding;
|
||||
this.plot = plot;
|
||||
this.opts = opts;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
|
||||
CanvasAxisLabel.prototype = new AxisLabel();
|
||||
CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
|
||||
function CanvasAxisLabel(axisName, position, padding, plot, opts) {
|
||||
AxisLabel.prototype.constructor.call(this, axisName, position, padding,
|
||||
plot, opts);
|
||||
}
|
||||
|
||||
CanvasAxisLabel.prototype.calculateSize = function() {
|
||||
if (!this.opts.axisLabelFontSizePixels)
|
||||
this.opts.axisLabelFontSizePixels = 14;
|
||||
if (!this.opts.axisLabelFontFamily)
|
||||
this.opts.axisLabelFontFamily = 'sans-serif';
|
||||
|
||||
var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
|
||||
var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
|
||||
if (this.position == 'left' || this.position == 'right') {
|
||||
this.width = this.opts.axisLabelFontSizePixels + this.padding;
|
||||
this.height = 0;
|
||||
} else {
|
||||
this.width = 0;
|
||||
this.height = this.opts.axisLabelFontSizePixels + this.padding;
|
||||
}
|
||||
};
|
||||
|
||||
CanvasAxisLabel.prototype.draw = function(box) {
|
||||
var ctx = this.plot.getCanvas().getContext('2d');
|
||||
ctx.save();
|
||||
ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
|
||||
this.opts.axisLabelFontFamily;
|
||||
var width = ctx.measureText(this.opts.axisLabel).width;
|
||||
var height = this.opts.axisLabelFontSizePixels;
|
||||
var x, y, angle = 0;
|
||||
if (this.position == 'top') {
|
||||
x = box.left + box.width/2 - width/2;
|
||||
y = box.top + height*0.72;
|
||||
} else if (this.position == 'bottom') {
|
||||
x = box.left + box.width/2 - width/2;
|
||||
y = box.top + box.height - height*0.72;
|
||||
} else if (this.position == 'left') {
|
||||
x = box.left + height*0.72;
|
||||
y = box.height/2 + box.top + width/2;
|
||||
angle = -Math.PI/2;
|
||||
} else if (this.position == 'right') {
|
||||
x = box.left + box.width - height*0.72;
|
||||
y = box.height/2 + box.top - width/2;
|
||||
angle = Math.PI/2;
|
||||
}
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(angle);
|
||||
ctx.fillText(this.opts.axisLabel, 0, 0);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
|
||||
HtmlAxisLabel.prototype = new AxisLabel();
|
||||
HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
|
||||
function HtmlAxisLabel(axisName, position, padding, plot, opts) {
|
||||
AxisLabel.prototype.constructor.call(this, axisName, position,
|
||||
padding, plot, opts);
|
||||
}
|
||||
|
||||
HtmlAxisLabel.prototype.calculateSize = function() {
|
||||
var elem = $('<div class="axisLabels" style="position:absolute;">' +
|
||||
this.opts.axisLabel + '</div>');
|
||||
this.plot.getPlaceholder().append(elem);
|
||||
// store height and width of label itself, for use in draw()
|
||||
this.labelWidth = elem.outerWidth(true);
|
||||
this.labelHeight = elem.outerHeight(true);
|
||||
elem.remove();
|
||||
|
||||
this.width = this.height = 0;
|
||||
if (this.position == 'left' || this.position == 'right') {
|
||||
this.width = this.labelWidth + this.padding;
|
||||
} else {
|
||||
this.height = this.labelHeight + this.padding;
|
||||
}
|
||||
};
|
||||
|
||||
HtmlAxisLabel.prototype.draw = function(box) {
|
||||
this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
|
||||
var elem = $('<div id="' + this.axisName +
|
||||
'Label" " class="axisLabels" style="position:absolute;">'
|
||||
+ this.opts.axisLabel + '</div>');
|
||||
this.plot.getPlaceholder().append(elem);
|
||||
if (this.position == 'top') {
|
||||
elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px');
|
||||
elem.css('top', box.top + 'px');
|
||||
} else if (this.position == 'bottom') {
|
||||
elem.css('left', box.left + box.width/2 - this.labelWidth/2 + 'px');
|
||||
elem.css('top', box.top + box.height - this.labelHeight + 'px');
|
||||
} else if (this.position == 'left') {
|
||||
elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px');
|
||||
elem.css('left', box.left + 'px');
|
||||
} else if (this.position == 'right') {
|
||||
elem.css('top', box.top + box.height/2 - this.labelHeight/2 + 'px');
|
||||
elem.css('left', box.left + box.width - this.labelWidth + 'px');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
CssTransformAxisLabel.prototype = new HtmlAxisLabel();
|
||||
CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
|
||||
function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
|
||||
HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
|
||||
padding, plot, opts);
|
||||
}
|
||||
|
||||
CssTransformAxisLabel.prototype.calculateSize = function() {
|
||||
HtmlAxisLabel.prototype.calculateSize.call(this);
|
||||
this.width = this.height = 0;
|
||||
if (this.position == 'left' || this.position == 'right') {
|
||||
this.width = this.labelHeight + this.padding;
|
||||
} else {
|
||||
this.height = this.labelHeight + this.padding;
|
||||
}
|
||||
};
|
||||
|
||||
CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
|
||||
var stransforms = {
|
||||
'-moz-transform': '',
|
||||
'-webkit-transform': '',
|
||||
'-o-transform': '',
|
||||
'-ms-transform': ''
|
||||
};
|
||||
if (x != 0 || y != 0) {
|
||||
var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
|
||||
stransforms['-moz-transform'] += stdTranslate;
|
||||
stransforms['-webkit-transform'] += stdTranslate;
|
||||
stransforms['-o-transform'] += stdTranslate;
|
||||
stransforms['-ms-transform'] += stdTranslate;
|
||||
}
|
||||
if (degrees != 0) {
|
||||
var rotation = degrees / 90;
|
||||
var stdRotate = ' rotate(' + degrees + 'deg)';
|
||||
stransforms['-moz-transform'] += stdRotate;
|
||||
stransforms['-webkit-transform'] += stdRotate;
|
||||
stransforms['-o-transform'] += stdRotate;
|
||||
stransforms['-ms-transform'] += stdRotate;
|
||||
}
|
||||
var s = 'top: 0; left: 0; ';
|
||||
for (var prop in stransforms) {
|
||||
if (stransforms[prop]) {
|
||||
s += prop + ':' + stransforms[prop] + ';';
|
||||
}
|
||||
}
|
||||
s += ';';
|
||||
return s;
|
||||
};
|
||||
|
||||
CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
|
||||
var offsets = { x: 0, y: 0, degrees: 0 };
|
||||
if (this.position == 'bottom') {
|
||||
offsets.x = box.left + box.width/2 - this.labelWidth/2;
|
||||
offsets.y = box.top + box.height - this.labelHeight;
|
||||
} else if (this.position == 'top') {
|
||||
offsets.x = box.left + box.width/2 - this.labelWidth/2;
|
||||
offsets.y = box.top;
|
||||
} else if (this.position == 'left') {
|
||||
offsets.degrees = -90;
|
||||
offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
|
||||
offsets.y = box.height/2 + box.top;
|
||||
} else if (this.position == 'right') {
|
||||
offsets.degrees = 90;
|
||||
offsets.x = box.left + box.width - this.labelWidth/2
|
||||
- this.labelHeight/2;
|
||||
offsets.y = box.height/2 + box.top;
|
||||
}
|
||||
return offsets;
|
||||
};
|
||||
|
||||
CssTransformAxisLabel.prototype.draw = function(box) {
|
||||
this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
|
||||
var offsets = this.calculateOffsets(box);
|
||||
var elem = $('<div class="axisLabels ' + this.axisName +
|
||||
'Label" style="position:absolute; ' +
|
||||
'color: ' + this.opts.color + '; ' +
|
||||
this.transforms(offsets.degrees, offsets.x, offsets.y) +
|
||||
'">' + this.opts.axisLabel + '</div>');
|
||||
this.plot.getPlaceholder().append(elem);
|
||||
};
|
||||
|
||||
|
||||
IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
|
||||
IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
|
||||
function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
|
||||
CssTransformAxisLabel.prototype.constructor.call(this, axisName,
|
||||
position, padding,
|
||||
plot, opts);
|
||||
this.requiresResize = false;
|
||||
}
|
||||
|
||||
IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
|
||||
// I didn't feel like learning the crazy Matrix stuff, so this uses
|
||||
// a combination of the rotation transform and CSS positioning.
|
||||
var s = '';
|
||||
if (degrees != 0) {
|
||||
var rotation = degrees/90;
|
||||
while (rotation < 0) {
|
||||
rotation += 4;
|
||||
}
|
||||
s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
|
||||
// see below
|
||||
this.requiresResize = (this.position == 'right');
|
||||
}
|
||||
if (x != 0) {
|
||||
s += 'left: ' + x + 'px; ';
|
||||
}
|
||||
if (y != 0) {
|
||||
s += 'top: ' + y + 'px; ';
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
|
||||
var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
|
||||
this, box);
|
||||
// adjust some values to take into account differences between
|
||||
// CSS and IE rotations.
|
||||
if (this.position == 'top') {
|
||||
// FIXME: not sure why, but placing this exactly at the top causes
|
||||
// the top axis label to flip to the bottom...
|
||||
offsets.y = box.top + 1;
|
||||
} else if (this.position == 'left') {
|
||||
offsets.x = box.left;
|
||||
offsets.y = box.height/2 + box.top - this.labelWidth/2;
|
||||
} else if (this.position == 'right') {
|
||||
offsets.x = box.left + box.width - this.labelHeight;
|
||||
offsets.y = box.height/2 + box.top - this.labelWidth/2;
|
||||
}
|
||||
return offsets;
|
||||
};
|
||||
|
||||
IeTransformAxisLabel.prototype.draw = function(box) {
|
||||
CssTransformAxisLabel.prototype.draw.call(this, box);
|
||||
if (this.requiresResize) {
|
||||
var elem = this.plot.getPlaceholder().find("." + this.axisName + "Label");
|
||||
// Since we used CSS positioning instead of transforms for
|
||||
// translating the element, and since the positioning is done
|
||||
// before any rotations, we have to reset the width and height
|
||||
// in case the browser wrapped the text (specifically for the
|
||||
// y2axis).
|
||||
elem.css('width', this.labelWidth);
|
||||
elem.css('height', this.labelHeight);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function init(plot) {
|
||||
// This is kind of a hack. There are no hooks in Flot between
|
||||
// the creation and measuring of the ticks (setTicks, measureTickLabels
|
||||
// in setupGrid() ) and the drawing of the ticks and plot box
|
||||
// (insertAxisLabels in setupGrid() ).
|
||||
//
|
||||
// Therefore, we use a trick where we run the draw routine twice:
|
||||
// the first time to get the tick measurements, so that we can change
|
||||
// them, and then have it draw it again.
|
||||
var secondPass = false;
|
||||
|
||||
var axisLabels = {};
|
||||
var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
|
||||
var defaultPadding = 2; // padding between axis and tick labels
|
||||
plot.hooks.draw.push(function (plot, ctx) {
|
||||
if (!secondPass) {
|
||||
// MEASURE AND SET OPTIONS
|
||||
$.each(plot.getAxes(), function(axisName, axis) {
|
||||
var opts = axis.options // Flot 0.7
|
||||
|| plot.getOptions()[axisName]; // Flot 0.6
|
||||
if (!opts || !opts.axisLabel || !axis.show)
|
||||
return;
|
||||
|
||||
var renderer = null;
|
||||
|
||||
if (!opts.axisLabelUseHtml &&
|
||||
navigator.appName == 'Microsoft Internet Explorer') {
|
||||
var ua = navigator.userAgent;
|
||||
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
|
||||
if (re.exec(ua) != null) {
|
||||
rv = parseFloat(RegExp.$1);
|
||||
}
|
||||
if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
|
||||
renderer = CssTransformAxisLabel;
|
||||
} else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
|
||||
renderer = IeTransformAxisLabel;
|
||||
} else if (opts.axisLabelUseCanvas) {
|
||||
renderer = CanvasAxisLabel;
|
||||
} else {
|
||||
renderer = HtmlAxisLabel;
|
||||
}
|
||||
} else {
|
||||
if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
|
||||
renderer = HtmlAxisLabel;
|
||||
} else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
|
||||
renderer = CanvasAxisLabel;
|
||||
} else {
|
||||
renderer = CssTransformAxisLabel;
|
||||
}
|
||||
}
|
||||
|
||||
var padding = opts.axisLabelPadding === undefined ?
|
||||
defaultPadding : opts.axisLabelPadding;
|
||||
|
||||
axisLabels[axisName] = new renderer(axisName,
|
||||
axis.position, padding,
|
||||
plot, opts);
|
||||
|
||||
// flot interprets axis.labelHeight and .labelWidth as
|
||||
// the height and width of the tick labels. We increase
|
||||
// these values to make room for the axis label and
|
||||
// padding.
|
||||
|
||||
axisLabels[axisName].calculateSize();
|
||||
|
||||
// AxisLabel.height and .width are the size of the
|
||||
// axis label and padding.
|
||||
axis.labelHeight += axisLabels[axisName].height;
|
||||
axis.labelWidth += axisLabels[axisName].width;
|
||||
opts.labelHeight = axis.labelHeight;
|
||||
opts.labelWidth = axis.labelWidth;
|
||||
});
|
||||
// re-draw with new label widths and heights
|
||||
secondPass = true;
|
||||
plot.setupGrid();
|
||||
plot.draw();
|
||||
} else {
|
||||
// DRAW
|
||||
$.each(plot.getAxes(), function(axisName, axis) {
|
||||
var opts = axis.options // Flot 0.7
|
||||
|| plot.getOptions()[axisName]; // Flot 0.6
|
||||
if (!opts || !opts.axisLabel || !axis.show)
|
||||
return;
|
||||
|
||||
axisLabels[axisName].draw(axis.box);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: 'axisLabels',
|
||||
version: '2.0b0'
|
||||
});
|
||||
})(jQuery);
|
||||
@@ -16,6 +16,7 @@ from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import has_access
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from static_replace import replace_urls
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import NotFoundError
|
||||
@@ -230,6 +231,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
system.set('DEBUG', settings.DEBUG)
|
||||
if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS') and instance_module is not None:
|
||||
system.set('psychometrics_handler', # set callback for updating PsychometricsData
|
||||
make_psychometrics_data_update_handler(instance_module))
|
||||
|
||||
try:
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
|
||||
@@ -27,6 +27,7 @@ from django.views.decorators.cache import cache_control
|
||||
from courseware import grades
|
||||
from courseware.access import has_access, get_access_group_name
|
||||
from courseware.courses import (get_course_with_access, get_courses_by_university)
|
||||
from psychometrics import psychoanalyze
|
||||
from student.models import UserProfile
|
||||
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
@@ -51,7 +52,18 @@ def instructor_dashboard(request, course_id):
|
||||
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
|
||||
|
||||
msg = ''
|
||||
# msg += ('POST=%s' % dict(request.POST)).replace('<','<')
|
||||
#msg += ('POST=%s' % dict(request.POST)).replace('<','<')
|
||||
|
||||
problems = []
|
||||
plots = []
|
||||
|
||||
# the instructor dashboard page is modal: grades, psychometrics, admin
|
||||
# keep that state in request.session (defaults to grades mode)
|
||||
idash_mode = request.POST.get('idash_mode','')
|
||||
if idash_mode:
|
||||
request.session['idash_mode'] = idash_mode
|
||||
else:
|
||||
idash_mode = request.session.get('idash_mode','Grades')
|
||||
|
||||
def escape(s):
|
||||
"""escape HTML special characters in string"""
|
||||
@@ -149,6 +161,9 @@ def instructor_dashboard(request, course_id):
|
||||
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
|
||||
return return_csv('answer_dist_%s.csv' % course_id, get_answers_distribution(request, course_id))
|
||||
|
||||
#----------------------------------------
|
||||
# Admin
|
||||
|
||||
elif 'List course staff' in action:
|
||||
group = get_staff_group(course)
|
||||
msg += 'Staff group = %s' % group.name
|
||||
@@ -187,14 +202,31 @@ def instructor_dashboard(request, course_id):
|
||||
user.groups.remove(group)
|
||||
track.views.server_track(request, 'remove-staff %s' % user, {}, page='idashboard')
|
||||
|
||||
# For now, mostly a static page
|
||||
#----------------------------------------
|
||||
# psychometrics
|
||||
|
||||
elif action == 'Generate Histogram and IRT Plot':
|
||||
problem = request.POST['Problem']
|
||||
nmsg, plots = psychoanalyze.generate_plots_for_problem(problem)
|
||||
msg += nmsg
|
||||
track.views.server_track(request, 'psychometrics %s' % problem, {}, page='idashboard')
|
||||
|
||||
if idash_mode=='Psychometrics':
|
||||
problems = psychoanalyze.problems_with_psychometric_data(course_id)
|
||||
|
||||
#----------------------------------------
|
||||
# context for rendering
|
||||
context = {'course': course,
|
||||
'staff_access': True,
|
||||
'admin_access': request.user.is_staff,
|
||||
'instructor_access': instructor_access,
|
||||
'datatable': datatable,
|
||||
'msg': msg,
|
||||
'modeflag': {idash_mode: 'selectedmode'},
|
||||
'problems': problems, # psychometrics
|
||||
'plots': plots, # psychometrics
|
||||
'course_errors': modulestore().get_item_errors(course.location),
|
||||
'djangopid' : os.getpid(),
|
||||
}
|
||||
|
||||
return render_to_response('courseware/instructor_dashboard.html', context)
|
||||
|
||||
@@ -35,7 +35,17 @@ def getip(request):
|
||||
ip = request.META.get('REMOTE_ADDR','None')
|
||||
return ip
|
||||
|
||||
def manage_modulestores(request,reload_dir=None):
|
||||
|
||||
def get_commit_id(course):
|
||||
return course.metadata.get('GIT_COMMIT_ID','No commit id')
|
||||
# getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id')
|
||||
|
||||
|
||||
def set_commit_id(course,commit_id):
|
||||
course.metadata['GIT_COMMIT_ID'] = commit_id
|
||||
# setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id)
|
||||
|
||||
def manage_modulestores(request, reload_dir=None, commit_id=None):
|
||||
'''
|
||||
Manage the static in-memory modulestores.
|
||||
|
||||
@@ -52,8 +62,9 @@ def manage_modulestores(request,reload_dir=None):
|
||||
ip = getip(request)
|
||||
|
||||
if LOCAL_DEBUG:
|
||||
html += '<h3>IP address: %s ' % ip
|
||||
html += '<h3>User: %s ' % request.user
|
||||
html += '<h3>IP address: %s <h3>' % ip
|
||||
html += '<h3>User: %s </h3>' % request.user
|
||||
html += '<h3>My pid: %s</h3>' % os.getpid()
|
||||
log.debug('request from ip=%s, user=%s' % (ip,request.user))
|
||||
|
||||
if not (ip in ALLOWED_IPS or 'any' in ALLOWED_IPS):
|
||||
@@ -66,14 +77,36 @@ def manage_modulestores(request,reload_dir=None):
|
||||
return HttpResponse(html, status=403)
|
||||
|
||||
#----------------------------------------
|
||||
# reload course if specified
|
||||
# reload course if specified; handle optional commit_id
|
||||
|
||||
if reload_dir is not None:
|
||||
if reload_dir not in def_ms.courses:
|
||||
html += '<h2 class="inline-error">Error: "%s" is not a valid course directory</h2>' % reload_dir
|
||||
else:
|
||||
html += '<h2>Reloaded course directory "%s"</h2>' % reload_dir
|
||||
def_ms.try_load_course(reload_dir)
|
||||
# reloading based on commit_id is needed when running mutiple worker threads,
|
||||
# so that a given thread doesn't reload the same commit multiple times
|
||||
current_commit_id = get_commit_id(def_ms.courses[reload_dir])
|
||||
log.debug('commit_id="%s"' % commit_id)
|
||||
log.debug('current_commit_id="%s"' % current_commit_id)
|
||||
|
||||
if (commit_id is not None) and (commit_id==current_commit_id):
|
||||
html += "<h2>Already at commit id %s for %s</h2>" % (commit_id, reload_dir)
|
||||
track.views.server_track(request,
|
||||
'reload %s skipped already at %s (pid=%s)' % (reload_dir,
|
||||
commit_id,
|
||||
os.getpid(),
|
||||
),
|
||||
{}, page='migrate')
|
||||
else:
|
||||
html += '<h2>Reloaded course directory "%s"</h2>' % reload_dir
|
||||
def_ms.try_load_course(reload_dir)
|
||||
gdir = settings.DATA_DIR / reload_dir
|
||||
new_commit_id = os.popen('cd %s; git log -n 1 | head -1' % gdir).read().strip().split(' ')[1]
|
||||
set_commit_id(def_ms.courses[reload_dir], new_commit_id)
|
||||
html += '<p>commit_id=%s</p>' % new_commit_id
|
||||
track.views.server_track(request, 'reloaded %s now at %s (pid=%s)' % (reload_dir,
|
||||
new_commit_id,
|
||||
os.getpid()), {}, page='migrate')
|
||||
|
||||
#----------------------------------------
|
||||
|
||||
@@ -94,6 +127,8 @@ def manage_modulestores(request,reload_dir=None):
|
||||
html += '<hr width="100%"/>'
|
||||
html += '<h2>Course: %s (%s)</h2>' % (course.display_name,cdir)
|
||||
|
||||
html += '<p>commit_id=%s</p>' % get_commit_id(course)
|
||||
|
||||
for field in dumpfields:
|
||||
data = getattr(course,field)
|
||||
html += '<h3>%s</h3>' % field
|
||||
|
||||
0
lms/djangoapps/psychometrics/__init__.py
Normal file
8
lms/djangoapps/psychometrics/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
'''
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from psychometrics.models import *
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(PsychometricData)
|
||||
0
lms/djangoapps/psychometrics/management/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# generate pyschometrics data from tracking logs and student module data
|
||||
|
||||
import os, sys, string
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from courseware.models import *
|
||||
from track.models import *
|
||||
from psychometrics.models import *
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
#db = "ocwtutor" # for debugging
|
||||
#db = "default"
|
||||
|
||||
db = getattr(settings,'DATABASE_FOR_PSYCHOMETRICS','default')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "initialize PsychometricData tables from StudentModule instances (and tracking data, if in SQL)."
|
||||
help += "Note this is done for all courses for which StudentModule instances exist."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# delete all pmd
|
||||
|
||||
#PsychometricData.objects.all().delete()
|
||||
#PsychometricData.objects.using(db).all().delete()
|
||||
|
||||
smset = StudentModule.objects.using(db).exclude(max_grade=None)
|
||||
|
||||
for sm in smset:
|
||||
url = sm.module_state_key
|
||||
location = Location(url)
|
||||
if not location.category=="problem":
|
||||
continue
|
||||
try:
|
||||
state = json.loads(sm.state)
|
||||
done = state['done']
|
||||
except:
|
||||
print "Oops, failed to eval state for %s (state=%s)" % (sm,sm.state)
|
||||
continue
|
||||
|
||||
if done: # only keep if problem completed
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
except PsychometricData.DoesNotExist:
|
||||
pmd = PsychometricData(studentmodule=sm)
|
||||
|
||||
pmd.done = done
|
||||
pmd.attempts = state['attempts']
|
||||
|
||||
# get attempt times from tracking log
|
||||
uname = sm.student.username
|
||||
tset = TrackingLog.objects.using(db).filter(username=uname, event_type__contains='save_problem_check')
|
||||
tset = tset.filter(event_source='server')
|
||||
tset = tset.filter(event__contains="'%s'" % url)
|
||||
checktimes = [x.dtcreated for x in tset]
|
||||
pmd.checktimes = checktimes
|
||||
if not len(checktimes)==pmd.attempts:
|
||||
print "Oops, mismatch in number of attempts and check times for %s" % pmd
|
||||
|
||||
#print pmd
|
||||
pmd.save(using=db)
|
||||
|
||||
print "%d PMD entries" % PsychometricData.objects.using(db).all().count()
|
||||
45
lms/djangoapps/psychometrics/models.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#
|
||||
# db model for psychometrics data
|
||||
#
|
||||
# this data is collected in real time
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from courseware.models import StudentModule
|
||||
|
||||
class PsychometricData(models.Model):
|
||||
"""
|
||||
This data is a table linking student, module, and module performance,
|
||||
including number of attempts, grade, max grade, and time of checks.
|
||||
|
||||
Links to instances of StudentModule, but only those for capa problems.
|
||||
|
||||
Note that StudentModule.module_state_key is nominally a Location instance (url string).
|
||||
That means it is of the form {tag}://{org}/{course}/{category}/{name}[@{revision}]
|
||||
and for capa problems, category = "problem".
|
||||
|
||||
checktimes is extracted from tracking logs, or added by capa module via psychometrics callback.
|
||||
"""
|
||||
|
||||
studentmodule = models.ForeignKey(StudentModule, db_index=True, unique=True) # contains student, module_state_key, course_id
|
||||
|
||||
done = models.BooleanField(default=False)
|
||||
attempts = models.IntegerField(default=0) # extracted from studentmodule.state
|
||||
checktimes = models.TextField(null=True, blank=True) # internally stored as list of datetime objects
|
||||
|
||||
# keep in mind
|
||||
# grade = studentmodule.grade
|
||||
# max_grade = studentmodule.max_grade
|
||||
# student = studentmodule.student
|
||||
# course_id = studentmodule.course_id
|
||||
# location = studentmodule.module_state_key
|
||||
|
||||
def __unicode__(self):
|
||||
sm = self.studentmodule
|
||||
return "[PsychometricData] %s url=%s, grade=%s, max=%s, attempts=%s, ct=%s" % (sm.student,
|
||||
sm.module_state_key,
|
||||
sm.grade,
|
||||
sm.max_grade,
|
||||
self.attempts,
|
||||
self.checktimes)
|
||||
|
||||
338
lms/djangoapps/psychometrics/psychoanalyze.py
Normal file
@@ -0,0 +1,338 @@
|
||||
#
|
||||
# File: psychometrics/psychoanalyze.py
|
||||
#
|
||||
# generate pyschometrics plots from PsychometricData
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
import math
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum, Max
|
||||
from psychometrics.models import *
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
log = logging.getLogger("mitx.psychometrics")
|
||||
|
||||
#db = "ocwtutor" # for debugging
|
||||
#db = "default"
|
||||
|
||||
db = getattr(settings, 'DATABASE_FOR_PSYCHOMETRICS', 'default')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# fit functions
|
||||
|
||||
|
||||
def func_2pl(x, a, b):
|
||||
"""
|
||||
2-parameter logistic function
|
||||
"""
|
||||
D = 1.7
|
||||
edax = np.exp(D * a * (x - b))
|
||||
return edax / (1 + edax)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# statistics class
|
||||
|
||||
|
||||
class StatVar(object):
|
||||
"""
|
||||
Simple statistics on floating point numbers: avg, sdv, var, min, max
|
||||
"""
|
||||
def __init__(self, unit=1):
|
||||
self.sum = 0
|
||||
self.sum2 = 0
|
||||
self.cnt = 0
|
||||
self.unit = unit
|
||||
self.min = None
|
||||
self.max = None
|
||||
|
||||
def add(self, x):
|
||||
if x is None:
|
||||
return
|
||||
if self.min is None:
|
||||
self.min = x
|
||||
else:
|
||||
if x < self.min:
|
||||
self.min = x
|
||||
if self.max is None:
|
||||
self.max = x
|
||||
else:
|
||||
if x > self.max:
|
||||
self.max = x
|
||||
self.sum += x
|
||||
self.sum2 += x**2
|
||||
self.cnt += 1
|
||||
|
||||
def avg(self):
|
||||
if self.cnt is None:
|
||||
return 0
|
||||
return self.sum / 1.0 / self.cnt / self.unit
|
||||
|
||||
def var(self):
|
||||
if self.cnt is None:
|
||||
return 0
|
||||
return (self.sum2 / 1.0 / self.cnt / (self.unit**2)) - (self.avg()**2)
|
||||
|
||||
def sdv(self):
|
||||
v = self.var()
|
||||
if v>0:
|
||||
return math.sqrt(v)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __str__(self):
|
||||
return 'cnt=%d, avg=%f, sdv=%f' % (self.cnt, self.avg(), self.sdv())
|
||||
|
||||
def __add__(self, x):
|
||||
self.add(x)
|
||||
return self
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# histogram generator
|
||||
|
||||
|
||||
def make_histogram(ydata, bins=None):
|
||||
'''
|
||||
Generate histogram of ydata using bins provided, or by default bins
|
||||
from 0 to 100 by 10. bins should be ordered in increasing order.
|
||||
|
||||
returns dict with keys being bins, and values being counts.
|
||||
special: hist['bins'] = bins
|
||||
'''
|
||||
if bins is None:
|
||||
bins = range(0, 100, 10)
|
||||
|
||||
nbins = len(bins)
|
||||
hist = dict(zip(bins, [0] * nbins))
|
||||
for y in ydata:
|
||||
for b in bins[::-1]: # in reverse order
|
||||
if y>b:
|
||||
hist[b] += 1
|
||||
break
|
||||
# hist['bins'] = bins
|
||||
return hist
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def problems_with_psychometric_data(course_id):
|
||||
'''
|
||||
Return dict of {problems (location urls): count} for which psychometric data is available.
|
||||
Does this for a given course_id.
|
||||
'''
|
||||
pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id)
|
||||
plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()]
|
||||
problems = dict( (p, pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist )
|
||||
|
||||
return problems
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def generate_plots_for_problem(problem):
|
||||
|
||||
pmdset = PsychometricData.objects.using(db).filter(studentmodule__module_state_key=problem)
|
||||
nstudents = pmdset.count()
|
||||
msg = ""
|
||||
plots = []
|
||||
|
||||
if nstudents < 2:
|
||||
msg += "%s nstudents=%d --> skipping, too few" % (problem, nstudents)
|
||||
return msg, plots
|
||||
|
||||
max_grade = pmdset[0].studentmodule.max_grade
|
||||
|
||||
agdat = pmdset.aggregate(Sum('attempts'), Max('attempts'))
|
||||
max_attempts = agdat['attempts__max']
|
||||
total_attempts = agdat['attempts__sum'] # not used yet
|
||||
|
||||
msg += "max attempts = %d" % max_attempts
|
||||
|
||||
xdat = range(1, max_attempts + 1)
|
||||
dataset = {'xdat': xdat}
|
||||
|
||||
# compute grade statistics
|
||||
grades = [pmd.studentmodule.grade for pmd in pmdset]
|
||||
gsv = StatVar()
|
||||
for g in grades:
|
||||
gsv += g
|
||||
msg += "<br><p><font color='blue'>Grade distribution: %s</font></p>" % gsv
|
||||
|
||||
# generate grade histogram
|
||||
ghist = []
|
||||
|
||||
axisopts = """{
|
||||
xaxes: [{
|
||||
axisLabel: 'Grade'
|
||||
}],
|
||||
yaxes: [{
|
||||
position: 'left',
|
||||
axisLabel: 'Count'
|
||||
}]
|
||||
}"""
|
||||
|
||||
if gsv.max > max_grade:
|
||||
msg += "<br/><p><font color='red'>Something is wrong: max_grade=%s, but max(grades)=%s</font></p>" % (max_grade, gsv.max)
|
||||
max_grade = gsv.max
|
||||
|
||||
if max_grade > 1:
|
||||
ghist = make_histogram(grades, np.linspace(0, max_grade, max_grade + 1))
|
||||
ghist_json = json.dumps(ghist.items())
|
||||
|
||||
plot = {'title': "Grade histogram for %s" % problem,
|
||||
'id': 'histogram',
|
||||
'info': '',
|
||||
'data': "var dhist = %s;\n" % ghist_json,
|
||||
'cmd': '[ {data: dhist, bars: { show: true, align: "center" }} ], %s' % axisopts,
|
||||
}
|
||||
plots.append(plot)
|
||||
else:
|
||||
msg += "<br/>Not generating histogram: max_grade=%s" % max_grade
|
||||
|
||||
# histogram of time differences between checks
|
||||
# Warning: this is inefficient - doesn't scale to large numbers of students
|
||||
dtset = [] # time differences in minutes
|
||||
dtsv = StatVar()
|
||||
for pmd in pmdset:
|
||||
try:
|
||||
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
|
||||
except:
|
||||
continue
|
||||
if len(checktimes) < 2:
|
||||
continue
|
||||
ct0 = checktimes[0]
|
||||
for ct in checktimes[1:]:
|
||||
dt = (ct - ct0).total_seconds() / 60.0
|
||||
if dt < 20: # ignore if dt too long
|
||||
dtset.append(dt)
|
||||
dtsv += dt
|
||||
ct0 = ct
|
||||
if dtsv.cnt > 2:
|
||||
msg += "<br/><p><font color='brown'>Time differences between checks: %s</font></p>" % dtsv
|
||||
bins = np.linspace(0, 1.5 * dtsv.sdv(), 30)
|
||||
dbar = bins[1] - bins[0]
|
||||
thist = make_histogram(dtset, bins)
|
||||
thist_json = json.dumps(sorted(thist.items(), key=lambda(x): x[0]))
|
||||
|
||||
axisopts = """{ xaxes: [{ axisLabel: 'Time (min)'}], yaxes: [{position: 'left',axisLabel: 'Count'}]}"""
|
||||
|
||||
plot = {'title': "Histogram of time differences between checks",
|
||||
'id': 'thistogram',
|
||||
'info': '',
|
||||
'data': "var thist = %s;\n" % thist_json,
|
||||
'cmd': '[ {data: thist, bars: { show: true, align: "center", barWidth:%f }} ], %s' % (dbar, axisopts),
|
||||
}
|
||||
plots.append(plot)
|
||||
|
||||
# one IRT plot curve for each grade received (TODO: this assumes integer grades)
|
||||
for grade in range(1, int(max_grade) + 1):
|
||||
yset = {}
|
||||
gset = pmdset.filter(studentmodule__grade=grade)
|
||||
ngset = gset.count()
|
||||
if ngset == 0:
|
||||
continue
|
||||
ydat = []
|
||||
ylast = 0
|
||||
for x in xdat:
|
||||
y = gset.filter(attempts=x).count() / ngset
|
||||
ydat.append( y + ylast )
|
||||
ylast = y + ylast
|
||||
yset['ydat'] = ydat
|
||||
|
||||
if len(ydat) > 3: # try to fit to logistic function if enough data points
|
||||
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
|
||||
yset['fitparam'] = cfp
|
||||
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
|
||||
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
|
||||
fitx = np.linspace(xdat[0], xdat[-1], 100)
|
||||
yset['fitx'] = fitx
|
||||
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
|
||||
|
||||
dataset['grade_%d' % grade] = yset
|
||||
|
||||
axisopts = """{
|
||||
xaxes: [{
|
||||
axisLabel: 'Number of Attempts'
|
||||
}],
|
||||
yaxes: [{
|
||||
max:1.0,
|
||||
position: 'left',
|
||||
axisLabel: 'Probability of correctness'
|
||||
}]
|
||||
}"""
|
||||
|
||||
# generate points for flot plot
|
||||
for grade in range(1, int(max_grade) + 1):
|
||||
jsdata = ""
|
||||
jsplots = []
|
||||
gkey = 'grade_%d' % grade
|
||||
if gkey in dataset:
|
||||
yset = dataset[gkey]
|
||||
jsdata += "var d%d = %s;\n" % (grade, json.dumps(zip(xdat, yset['ydat'])))
|
||||
jsplots.append('{ data: d%d, lines: { show: false }, points: { show: true}, color: "red" }' % grade)
|
||||
if 'fitpts' in yset:
|
||||
jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'], yset['fity'])))
|
||||
jsplots.append('{ data: fit, lines: { show: true }, color: "blue" }')
|
||||
(a, b) = yset['fitparam'][0]
|
||||
irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a, b)
|
||||
else:
|
||||
irtinfo = ""
|
||||
|
||||
plots.append({'title': 'IRT Plot for grade=%s %s' % (grade, irtinfo),
|
||||
'id': "irt%s" % grade,
|
||||
'info': '',
|
||||
'data': jsdata,
|
||||
'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
|
||||
})
|
||||
|
||||
#log.debug('plots = %s' % plots)
|
||||
return msg, plots
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_psychometrics_data_update_handler(studentmodule):
|
||||
"""
|
||||
Construct and return a procedure which may be called to update
|
||||
the PsychometricsData instance for the given StudentModule instance.
|
||||
"""
|
||||
sm = studentmodule
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
except PsychometricData.DoesNotExist:
|
||||
pmd = PsychometricData(studentmodule=sm)
|
||||
|
||||
def psychometrics_data_update_handler(state):
|
||||
"""
|
||||
This function may be called each time a problem is successfully checked
|
||||
(eg on save_problem_check events in capa_module).
|
||||
|
||||
state = instance state (a nice, uniform way to interface - for more future psychometric feature extraction)
|
||||
"""
|
||||
try:
|
||||
state = json.loads(sm.state)
|
||||
done = state['done']
|
||||
except:
|
||||
log.exception("Oops, failed to eval state for %s (state=%s)" % (sm, sm.state))
|
||||
return
|
||||
|
||||
pmd.done = done
|
||||
pmd.attempts = state['attempts']
|
||||
try:
|
||||
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
|
||||
except:
|
||||
checktimes = []
|
||||
checktimes.append(datetime.datetime.now())
|
||||
pmd.checktimes = checktimes
|
||||
try:
|
||||
pmd.save()
|
||||
except:
|
||||
log.exception("Error in updating psychometrics data for %s" % sm)
|
||||
|
||||
return psychometrics_data_update_handler
|
||||
@@ -71,6 +71,8 @@ MITX_FEATURES = {
|
||||
'ENABLE_DISCUSSION' : False,
|
||||
'ENABLE_DISCUSSION_SERVICE': True,
|
||||
|
||||
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
|
||||
'ENABLE_SQL_TRACKING_LOGS': False,
|
||||
'ENABLE_LMS_MIGRATION': False,
|
||||
'ENABLE_MANUAL_GIT_RELOAD': False,
|
||||
@@ -619,6 +621,7 @@ INSTALLED_APPS = (
|
||||
'util',
|
||||
'certificates',
|
||||
'instructor',
|
||||
'psychometrics',
|
||||
|
||||
#For the wiki
|
||||
'wiki', # The new django-wiki from benjaoming
|
||||
|
||||
@@ -20,6 +20,8 @@ MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains-
|
||||
MITX_FEATURES['SUBDOMAIN_BRANDING'] = True
|
||||
MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST)
|
||||
MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
|
||||
MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = True # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
|
||||
|
||||
WIKI_ENABLED = True
|
||||
|
||||
|
||||
BIN
lms/static/images/press/baltsun_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
lms/static/images/press/bostinno_logo_178x138.jpg
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
lms/static/images/press/bostonmag_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/images/press/csmonitor_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
lms/static/images/press/insidehighered_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
lms/static/images/press/itbriefing_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
lms/static/images/press/radioboston_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
lms/static/images/press/smartplanet_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
lms/static/images/press/techreview_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
lms/static/images/press/thetech_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
lms/static/images/press/time_logo_178x138.jpg
Executable file
|
After Width: | Height: | Size: 4.0 KiB |
@@ -25,11 +25,6 @@ div.info-wrapper {
|
||||
margin-bottom: lh();
|
||||
padding-bottom: lh(.5);
|
||||
|
||||
&:first-child {
|
||||
margin: 0 (-(lh(.5))) lh();
|
||||
padding: lh(.5);
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: 0;
|
||||
list-style-type: disk;
|
||||
|
||||
@@ -80,7 +80,6 @@ div.course-wrapper {
|
||||
}
|
||||
|
||||
.histogram {
|
||||
display: none;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
}
|
||||
@@ -117,6 +116,7 @@ div.course-wrapper {
|
||||
margin: 0;
|
||||
@include clearfix();
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
width: flex-grid(3, 9);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/xml/xml.js')}"></script>
|
||||
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/python/python.js')}"></script>
|
||||
|
||||
|
||||
<%static:js group='courseware'/>
|
||||
<%static:js group='discussion'/>
|
||||
|
||||
@@ -35,6 +36,22 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
var $$course_id = "${course.id}";
|
||||
|
||||
$(function(){
|
||||
$(".ui-accordion-header a").each(function() {
|
||||
var wordArray = $(this).text().split(" ");
|
||||
var finalTitle = "";
|
||||
for (i=0;i<=wordArray.length-1;i++) {
|
||||
finalTitle += wordArray[i];
|
||||
if (i == (wordArray.length-2)) {
|
||||
finalTitle += " ";
|
||||
} else {
|
||||
finalTitle += " ";
|
||||
}
|
||||
}
|
||||
$(this).html(finalTitle);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='instructor'" />
|
||||
@@ -31,37 +33,91 @@ table.stat_table td {
|
||||
border-color: #666666;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a.selectedmode { background-color: yellow; }
|
||||
|
||||
</style>
|
||||
|
||||
<script language="JavaScript" type="text/javascript">
|
||||
function goto( mode)
|
||||
{
|
||||
document.idashform.idash_mode.value = mode;
|
||||
document.idashform.submit() ;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="container">
|
||||
<div class="instructor-dashboard-wrapper">
|
||||
|
||||
<section class="instructor-dashboard-content">
|
||||
<h1>Instructor Dashboard</h1>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<h2>[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
|
||||
%if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'):
|
||||
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
|
||||
%endif
|
||||
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> ]
|
||||
</h2>
|
||||
|
||||
<div style="text-align:right" id="djangopid">${djangopid}</div>
|
||||
|
||||
<form name="idashform" method="POST">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="hidden" name="idash_mode" value="">
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Grades'):
|
||||
<p>
|
||||
<a href="${reverse('gradebook', kwargs=dict(course_id=course.id))}">Gradebook</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="${reverse('grade_summary', kwargs=dict(course_id=course.id))}">Grade summary</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump list of enrolled students">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump Grades for all students in this course">
|
||||
<input type="submit" name="action" value="Download CSV of all student grades for this course">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Dump all RAW grades for all students in this course">
|
||||
<input type="submit" name="action" value="Download CSV of all RAW grades">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of answer distributions">
|
||||
</p>
|
||||
%endif
|
||||
|
||||
%if instructor_access:
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
<p>Select a problem and an action:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<select name="Problem">
|
||||
%for problem, count in sorted(problems.items(), key=lambda x: x[0]):
|
||||
<option value="${problem}">${problem} [${count}]</option>
|
||||
%endfor
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Generate Histogram and IRT Plot">
|
||||
</p>
|
||||
|
||||
<p></p>
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
%if instructor_access:
|
||||
<hr width="40%" style="align:left">
|
||||
<p>
|
||||
<input type="submit" name="action" value="List course staff members">
|
||||
@@ -69,16 +125,20 @@ table.stat_table td {
|
||||
<input type="text" name="staffuser"> <input type="submit" name="action" value="Remove course staff">
|
||||
<input type="submit" name="action" value="Add course staff">
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
|
||||
%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
|
||||
<p>
|
||||
<input type="submit" name="action" value="Reload course from XML files">
|
||||
<input type="submit" name="action" value="GIT pull and Reload course">
|
||||
%endif
|
||||
%endif
|
||||
|
||||
</form>
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics') is None:
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<p>
|
||||
@@ -99,14 +159,45 @@ table.stat_table td {
|
||||
%endfor
|
||||
</table>
|
||||
</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Psychometrics'):
|
||||
|
||||
%for plot in plots:
|
||||
<br/>
|
||||
<h3>${plot['title']}</h3>
|
||||
<br/>
|
||||
<p>${plot['info']}</p>
|
||||
<br/>
|
||||
<div id="plot_${plot['id']}" style="width:600px;height:300px;"></div>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
${plot['data']}
|
||||
$.plot($("#plot_${plot['id']}"), ${plot['cmd']} );
|
||||
});
|
||||
</script>
|
||||
<br/>
|
||||
<br/>
|
||||
%endfor
|
||||
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
## always show msg
|
||||
|
||||
%if msg:
|
||||
<p>${msg}</p>
|
||||
%endif
|
||||
|
||||
% if course_errors is not UNDEFINED:
|
||||
##-----------------------------------------------------------------------------
|
||||
%if modeflag.get('Admin'):
|
||||
% if course_errors is not UNDEFINED:
|
||||
<h2>Course errors</h2>
|
||||
<div id="course-errors">
|
||||
%if not course_errors:
|
||||
None
|
||||
%else:
|
||||
<ul>
|
||||
% for (summary, err) in course_errors:
|
||||
<li>${summary | h}
|
||||
@@ -118,8 +209,10 @@ table.stat_table td {
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
%endif
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
%endif
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,11 @@
|
||||
<div id="register_message"></div>
|
||||
%endif
|
||||
%else:
|
||||
<a href="#signup-modal" class="register" rel="leanModal" data-notice='You must Sign Up or <a href="#login-modal" rel="leanModal">Log In</a> to enroll.'>Register for ${course.number}</a>
|
||||
<a href="#signup-modal" class="register" rel="leanModal" data-notice='You must Sign Up
|
||||
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
|
||||
or <a href="#login-modal" rel="leanModal">Log In</a>
|
||||
% endif
|
||||
to enroll.'>Register for ${course.number}</a>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,187 @@
|
||||
[
|
||||
|
||||
{
|
||||
"title": "Is MIT Giving Away the Farm?",
|
||||
"url": "http://www.technologyreview.com/mitnews/428698/is-mit-giving-away-the-farm/",
|
||||
"author": "Larry Hardesty",
|
||||
"image": "techreview_logo_178x138.jpg",
|
||||
"deck": "The surprising logic of MIT's free online education program.",
|
||||
"publication": "Technology Review",
|
||||
"publish_date": "September/October 2012"
|
||||
},
|
||||
{
|
||||
"title": "School’s Out, Forever",
|
||||
"url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/",
|
||||
"author": "Chris Vogel",
|
||||
"image": "bostonmag_logo_178x138.jpg",
|
||||
"deck": "A new online education program from Harvard and MIT is poised to transform what it means to go to college.",
|
||||
"publication": "Boston Magazine",
|
||||
"publish_date": "September 2012"
|
||||
},
|
||||
{
|
||||
"title": "Q&A: Anant Agarwal, edX’s president and first professor",
|
||||
"url": "http://www.bostonmagazine.com/articles/2012/08/edx-online-classes-schools-ou
t-forever/",
|
||||
"author": " Molly Petrilla ",
|
||||
"image": "smartplanet_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Smart Planet",
|
||||
"publish_date": "September 3, 2012"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "EdX To Offer Proctored Final Exam For One Course",
|
||||
"url": "http://www.thecrimson.com/article/2012/9/7/edx-offer-proctored-exams/",
|
||||
"author": "Samuel Y. Weinstock",
|
||||
"image": "harvardcrimson_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "Harvard Crimson",
|
||||
"publish_date": "September 7, 2012"
|
||||
},
|
||||
{
|
||||
"title": "MOOCing On Site",
|
||||
"url": "http://www.insidehighered.com/news/2012/09/07/site-based-testing-deals-strengthen-case-granting-credit-mooc-students",
|
||||
"author": "Steve Kolowich ",
|
||||
"image": "insidehighered_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Inside Higher Education",
|
||||
"publish_date": "September 7, 2012"
|
||||
},
|
||||
{
|
||||
"title": "edX Curbs the Downfalls of Online Education By Announcing Supervised Final Exams",
|
||||
"url": "http://bostinno.com/2012/09/07/edx-pearson-proctored-exams/",
|
||||
"author": "Lauren Landry",
|
||||
"image": "bostinno_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Bostinno",
|
||||
"publish_date": "September 7, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard and MIT online courses get 'real world' exams",
|
||||
"url": "http://www.bbc.co.uk/news/education-19505776",
|
||||
"author": "Sean Coughlan",
|
||||
"image": "bbc_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "BBC",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard-MIT Online School EdX to Offer Supervised Final Exams",
|
||||
"url": "http://www.businessweek.com/news/2012-09-06/harvard-mit-online-school-edx-to-offer-supervised-final-exams",
|
||||
"author": "Oliver Staley",
|
||||
"image": "bloomberg_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "Bloomberg Business Week",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Colorado State to Offer Credits for Online Class",
|
||||
"url": "http://www.nytimes.com/2012/09/07/education/colorado-state-to-offer-credits-for-online-class.html?_r=3",
|
||||
"author": "Tamar Lewin",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "New York Times",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
{
|
||||
"title": "edX Offers Proctored Exams for Open Online Course",
|
||||
"url": "http://chronicle.com/blogs/wiredcampus/edx-offers-proctored-exams-for-open-online-course/39656",
|
||||
"author": " Marc Parry",
|
||||
"image": "chroniclehighered_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "Chronicle of Higher Education",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
{
|
||||
"title": "edX Offers Proctored Exams for Open Online Course",
|
||||
"url": "http://itbriefing.net/modules.php?op=modload&name=News&file=article&sid=323229&newlang=eng&topic=15&catid=37",
|
||||
"author": "",
|
||||
"image": "itbriefing_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "ITBriefing.net",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Student Loans: Debt for Life",
|
||||
"url": "http://www.businessweek.com/articles/2012-09-06/student-loans-debt-for-life#p3",
|
||||
"author": "Peter Coy",
|
||||
"image": "bloomberg_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "Bloomberg Business Week",
|
||||
"publish_date": "September 6, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Straighterline wants to help professors expand reach, while students save",
|
||||
"url": "http://www.baltimoresun.com/business/technology/blog/bs-bz-straighterline-college-professors-20120904,0,6114022.story",
|
||||
"author": "Gus G. Sentementes",
|
||||
"image": "baltsun_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "The Baltimore Sun",
|
||||
"publish_date": "September 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Want to be a reporter? Learn to code",
|
||||
"url": "http://gigaom.com/cloud/want-to-be-a-reporter-learn-to-code/",
|
||||
"author": "Barb Darrow",
|
||||
"image": "gigaom_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "GigaOM",
|
||||
"publish_date": "September 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "MOOC Brigade: Will Massive, Open Online Courses Revolutionize Higher Education?",
|
||||
"url": "http://nation.time.com/2012/09/04/mooc-brigade-will-massive-open-online-courses-revolutionize-higher-education/",
|
||||
"author": "Kayla Webley",
|
||||
"image": "time_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Time",
|
||||
"publish_date": "September 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Ivy walls lower with free online classes from Coursera and edX ",
|
||||
"url": "http://www.csmonitor.com/Innovation/Pioneers/2012/0903/Ivy-walls-lower-with-free-online-classes-from-Coursera-and-edX",
|
||||
"author": "Chris Gaylord",
|
||||
"image": "csmonitor_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Christian Science Monitor",
|
||||
"publish_date": "September 3, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Summer recap. RLADs, new edX partner, Institute files amicus brief",
|
||||
"url": "http://tech.mit.edu/V132/N34/summer.html",
|
||||
"author": "",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "The Tech",
|
||||
"publish_date": "September 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Into the Future With MOOC's",
|
||||
"url": "http://chronicle.com/article/Into-the-Future-With-MOOCs/134080/",
|
||||
"author": "Kevin Carey",
|
||||
"image": "chroniclehighered_logo_178x138.jpeg",
|
||||
"deck": "",
|
||||
"publication": "The Chronicle of Higher Education",
|
||||
"publish_date": "September 3, 2012"
|
||||
},
|
||||
{
|
||||
"title": "The Future Of Higher Education",
|
||||
"url": "http://radioboston.wbur.org/2012/08/20/higher-education-online",
|
||||
"author": "",
|
||||
"image": "radioboston_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "NPR/Radio Boston",
|
||||
"publish_date": "August 20, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Berkeley Joins edX",
|
||||
"url": "http://www.insidehighered.com/quicktakes/2012/07/24/berkeley-joins-edx",
|
||||
"author": "Tamar Lewin",
|
||||
"image": "insidehighered_logo_178x138.jpg",
|
||||
"deck": "",
|
||||
"publication": "Inside Higher Ed",
|
||||
"publish_date": "July 24, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Berkeley to Join the Free Online Learning Partnership EdX",
|
||||
"url": "http://www.nytimes.com/2012/07/24/education/berkeley-to-offer-free-online-classes-on-edx.html?_r=1",
|
||||
|
||||
@@ -37,4 +37,3 @@
|
||||
% endfor
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
urlpatterns += (
|
||||
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^migrate/reload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^migrate/reload/(?P<reload_dir>[^/]+)/(?P<commit_id>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
|
||||
url(r'^gitreload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.gitreload'),
|
||||
)
|
||||
|
||||