Commit 5edbf3d8 by cahrens

Merge branch 'master' into feature/christina/metadata-ui

parents 68cd922e cb775ffa
from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand
......@@ -6,4 +7,4 @@ class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
update_templates()
update_templates(modulestore('direct'))
......@@ -937,7 +937,7 @@ class TemplateTestCase(ModuleStoreTestCase):
self.assertIsNotNone(verify_create)
# now run cleanup
update_templates()
update_templates(modulestore('direct'))
# now try to find dangling template, it should not be in DB any longer
asserted = False
......
......@@ -72,7 +72,7 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
......
......@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value
......
......@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = (
('edX Admins', 'admin@edx.org'),
)
......
......@@ -895,4 +895,4 @@ function saveSetSectionScheduleDate(e) {
hideModal();
});
}
\ No newline at end of file
}
......@@ -2,13 +2,13 @@
* Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
* did not occur on the event.currentTarget.
*
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* did not occur on the event.currentTarget.
*
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
*
*
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/
......@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); },
function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION);
event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
......@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) {
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
}
event.data.timeoutEventId = null;
};
\ No newline at end of file
};
......@@ -80,6 +80,6 @@ $(document).ready(function(){
$('section.problem-edit').show();
return false;
});
});
// single per course holds the updates and handouts
// single per course holds the updates and handouts
CMS.Models.CourseInfo = Backbone.Model.extend({
// This model class is not suited for restful operations and is considered just a server side initialized container
url: '',
defaults: {
"courseId": "", // the location url
"updates" : null, // UpdateCollection
"handouts": null // HandoutCollection
},
idAttribute : "courseId"
});
// course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: {
......@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({
*/
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
url : function() {return this.urlbase + "course_info/updates/";},
model : CMS.Models.CourseUpdate
});
\ No newline at end of file
......@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({
},
_tagPattern : /[^:]+/g,
_fieldPattern : new RegExp('[^/]+','g'),
parse: function(payload) {
if (_.isArray(payload)) {
return {
......@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({
course: payload[2],
category: payload[3],
name: payload[4]
}
};
}
else if (_.isString(payload)) {
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
......@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative
});
\ No newline at end of file
});
......@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
"data": null,
"metadata" : null,
"children" : null
},
});
\ No newline at end of file
}
});
......@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {};
......@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
};
Backbone.Model.prototype.save.call(this, attrs, options);
},
afterSave : function(self) {
// remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) {
......
......@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
......
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
defaults : {
defaults : {
course_location : null,
graders : null, // CourseGraderCollection
graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...}
},
......@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1,
"drop_count" : 0,
"short_label" : "", // what to use in place of type if space is an issue
"short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100
},
parse : function(attrs) {
......@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
}
});
\ No newline at end of file
});
......@@ -93,4 +93,4 @@ CMS.Views.Checklists = Backbone.View.extend({
error : CMS.ServerError
});
}
});
\ No newline at end of file
});
......@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"click .post-actions > .edit-button" : "onEdit",
"click .post-actions > .delete-button" : "onDelete"
},
initialize: function() {
var self = this;
// instantiates an editor template for each update in the collection
......@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"/static/client_templates/course_info_update.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
self.render();
}
);
// when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render);
},
render: function () {
// iterate over updates and create views for each using the template
var updateEle = this.$el.find("#course-update-list");
......@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this;
},
onNew: function(event) {
event.preventDefault();
var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0});
var $newForm = $(this.template({ updateModel : newModel }));
var updateEle = this.$el.find("#course-update-list");
......@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
lineWrapping: true,
});
}
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
......@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$('.date').datepicker('destroy');
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
},
onSave: function(event) {
event.preventDefault();
var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change
// push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError});
this.closeEditor(this);
analytics.track('Saved Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
'date': this.dateEntry(event).val()
});
},
onCancel: function(event) {
event.preventDefault();
// change editor contents back to model values and hide the editor
......@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id);
},
onEdit: function(event) {
event.preventDefault();
var self = this;
this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing');
$(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) {
......@@ -154,13 +154,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
analytics.track('Deleted Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
'date': this.dateEntry(event).val()
});
var targetModel = this.eventModel(event);
this.modelDom(event).remove();
var cacheThis = this;
targetModel.destroy({success : function (model, response) {
targetModel.destroy({success : function (model, response) {
cacheThis.collection.fetch({success : function() {cacheThis.render();},
error : CMS.ServerError});
},
......@@ -192,17 +192,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
},
// Dereferencing from events to screen elements
// Dereferencing from events to screen elements
eventModel: function(event) {
// not sure if it should be currentTarget or delegateTarget
return this.collection.get($(event.currentTarget).attr("name"));
},
modelDom: function(event) {
return $(event.currentTarget).closest("li");
},
editor: function(event) {
var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("form").first();
......@@ -216,7 +216,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentEntry: function(event) {
return $(event.currentTarget).closest("li").find(".new-update-content").first();
},
dateDisplay: function(event) {
return $(event.currentTarget).closest("li").find("#date-display").first();
},
......@@ -224,7 +224,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentDisplay: function(event) {
return $(event.currentTarget).closest("li").find(".update-contents").first();
}
});
// the handouts view is dumb right now; it needs tied to a model and all that jazz
......@@ -245,7 +245,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
self.render();
}
);
},
......@@ -253,8 +253,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
}
);
},
render: function () {
render: function () {
var updateEle = this.$el;
var self = this;
this.$el.html(
......@@ -313,4 +313,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
self.$form.find('.CodeMirror').remove();
this.$codeMirror = null;
}
});
\ No newline at end of file
});
......@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({
urlRoot : function() {
if (this.has('location')) {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/';
}
else return "";
......@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
'<% if (!hideSymbol) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' +
'</a>' +
'<ul class="menu">' +
'<ul class="menu">' +
'<% graders.each(function(option) { %>' +
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
'<% }) %>' +
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>');
this.assignmentGrade = new CMS.Models.AssignmentGrade({
assignmentUrl : this.$el.closest('.id-holder').data('id'),
assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null
this.graders = this.options['graders'];
......@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
},
selectGradeType : function(e) {
e.preventDefault();
this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text());
this.render();
}
})
\ No newline at end of file
})
......@@ -6,26 +6,26 @@ $(document).ready(function() {
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
zIndex: 999,
zIndex: 999,
start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future
drag: generateCheckHoverState('.collapsed', ''),
drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
zIndex: 999,
zIndex: 999,
start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
......@@ -33,8 +33,8 @@ $(document).ready(function() {
stack: '.courseware-section',
revert: "invalid"
});
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
......@@ -50,7 +50,7 @@ $(document).ready(function() {
drop: onSubsectionReordered,
greedy: true
});
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
......@@ -58,7 +58,7 @@ $(document).ready(function() {
drop: onSectionReordered,
greedy: true
});
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
......@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) {
$.extend(droppable, {offset : $(droppable).offset()});
var t = droppable.offset.top,
var t = droppable.offset.top,
b = t + droppable.proportions.height;
if (t === b) {
......@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
$(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) {
console.log('not up', $(this).data('id'));
......@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup');
}
var intersectsTop = computeIntersection(this, ui.helper,
var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) {
console.log('not down', $(this).data('id'));
......@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown');
}
});
}
}
......@@ -159,20 +159,20 @@ function removeHesitate(event, ui) {
}
function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400);
$(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
// figure out where it came from and where it slots in.
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
// figure out where it came from and where it slots in.
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
......@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) {
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in.
// figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
......
CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText);
};
\ No newline at end of file
};
CMS.Views.ValidatingView = Backbone.View.extend({
// Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how
// Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError);
......@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
"change textarea" : "clearValidationErrors"
},
fieldToSelectorMap : {
// Your subclass must populate this w/ all of the model keys and dom selectors
// Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
......
......@@ -6,7 +6,7 @@
@include box-sizing(border-box);
.copy {
@include font-size(13);
@extend .t-copy-sub2;
}
}
......@@ -184,12 +184,12 @@
}
.action-primary {
@include font-size(13);
@extend .t-action3;
font-weight: 600;
}
.action-secondary {
@include font-size(13);
@extend .t-action3;
}
}
}
......@@ -367,12 +367,12 @@
}
.copy {
@include font-size(13);
@extend .t-copy-sub2;
width: flex-grid(10, 12);
color: $gray-l2;
.title {
@include font-size(14);
@extend .t-title-4;
margin-bottom: 0;
color: $white;
}
......@@ -409,13 +409,13 @@
.action-primary {
@include blue-button();
@include font-size(13);
@extend .t-action3;
border-color: $blue-d2;
font-weight: 600;
}
.action-secondary {
@include font-size(13);
@extend .t-action3;
}
}
......@@ -504,7 +504,7 @@
// adopted alerts
.alert {
@include font-size(14);
@extend .t-copy-sub2;
@include box-sizing(border-box);
@include clearfix();
margin: 0 auto;
......@@ -530,7 +530,7 @@
}
.copy {
@include font-size(13);
@extend .t-copy-sub2;
width: flex-grid(10, 12);
color: $gray-l2;
......@@ -568,12 +568,12 @@
}
.action-primary {
@include font-size(13);
@extend .t-action3;
font-weight: 600;
}
.action-secondary {
@include font-size(13);
@extend .t-action3;
}
}
}
......@@ -730,7 +730,7 @@ body.uxdesign.alerts {
border-radius: 3px;
background: #fbf6e1;
// background: #edbd3c;
font-size: 14px;
@extend .t-copy-sub1;
@include clearfix;
.alert-message {
......
......@@ -2,7 +2,7 @@
// ====================
// headings/titles
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5, .t-title-5 {
.t-title-1, .t-title-2, .t-title-3, .t-title-4, .t-title-5 {
color: $gray-d3;
}
......@@ -21,7 +21,7 @@
}
.t-title-4 {
@include font-size(14);
}
.t-title-5 {
......@@ -82,4 +82,4 @@
// misc
.t-icon {
line-height: 0;
}
\ No newline at end of file
}
......@@ -114,6 +114,7 @@
<li><a href="#alert-announcement2" class="show-alert">Show Announcement</a></li>
<li><a href="#alert-announcement1" class="show-alert">Show Announcement with Actions</a></li>
<li><a href="#alert-activation" class="show-alert">Show Activiation</a></li>
<li><a href="#alert-threeActions" class="show-alert">Alert with three actions</a></li>
</ul>
</section>
......@@ -129,6 +130,10 @@
<ul>
<li>
<a href="#notification-changesMade" class="show-notification">Show Changes Made (used in Advanced Settings)</a>
<a href="#notification-changesMade" class="hide-notification">Hide Changes Made (used in Advanced Settings)</a>
</li>
<li>
<a href="#notification-change" class="show-notification">Show Change Warning</a>
<a href="#notification-change" class="hide-notification">Hide Change Warning</a>
</li>
......@@ -151,6 +156,10 @@
<a href="#notification-help" class="show-notification">Show Help</a>
<a href="#notification-help" class="hide-notification">Hide Help</a>
</li>
<li>
<a href="#notification-threeActions" class="show-notification">Show Notification with three actions</a>
<a href="#notification-threeActions" class="hide-notification">Hide Notification with three actions</a>
</li>
</ul>
</section>
......@@ -182,6 +191,33 @@
</%block>
<%block name="view_alerts">
<!-- alert: 3 actions -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-threeActions">
<div class="alert warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3">You are editing a draft</h2>
<p class="message">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action action-save action-primary">Save Draft</a>
</li>
<li class="nav-item">
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
</li>
<li class="nav-item">
<a href="#" class="action action-secondary">Do Something Elsee</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- alert: you're editing a draft -->
<div class="wrapper wrapper-alert wrapper-alert-warning" id="alert-draft">
<div class="alert warning has-actions">
......@@ -196,10 +232,10 @@
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button save-button action-primary">Save Draft</a>
<a href="#" class="action action-save action-primary">Save Draft</a>
</li>
<li class="nav-item">
<a href="#" class="button cancel-button action-secondary">Disgard Draft</a>
<a href="#" class="action action-cancel action-secondary">Disgard Draft</a>
</li>
</ul>
</nav>
......@@ -220,10 +256,10 @@
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
</li>
<li class="nav-item">
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
</li>
</ul>
</nav>
......@@ -297,7 +333,7 @@
<h3 class="sr">Alert Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button cancel-button action-primary">Cancel Your Submission</a>
<a href="#" class="action action-cancel action-primary">Cancel Your Submission</a>
</li>
</ul>
</nav>
......@@ -367,13 +403,13 @@
<%block name="view_notifications">
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-change" id="notification-change" role="status">
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-change-title" aria-describedby="notification-change-description" id="notification-change">
<div class="notification change has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i>
<div class="copy">
<h2 class="title title-3">You've Made Some Changes</h2>
<p class="message">Your changes will not take effect until you <strong>save your progress</strong>.</p>
<h2 class="title title-3" id="notification-change-title">You've Made Some Changes</h2>
<p class="message" id="notification-change-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
</div>
<nav class="nav-actions">
......@@ -390,6 +426,57 @@
</div>
</div>
<!-- notification: three actions example -->
<div class="wrapper wrapper-notification wrapper-notification-change" aria-hidden="true" role="dialog" aria-labelledby="notification-threeActions-title" aria-describedby="notification-threeActions-description" id="notification-threeActions">
<div class="notification change has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-change">&#x1F4DD;</i>
<div class="copy">
<h2 class="title title-3" id="notification-threeActions-title">You've Made Some Changes</h2>
<p class="message" id="notification-threeActions-description">Your changes will not take effect until you <strong>save your progress</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary">Save Changes</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Don't Save</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">Do something else</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description" id="notification-changesMade">
<div class="notification warning has-actions">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<div class="copy">
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="" class="action action-save action-primary">Save Changes</a>
</li>
<li class="nav-item">
<a href="" class="action action-cancel action-secondary">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- notification: newer version exists -->
<div class="wrapper wrapper-notification wrapper-notification-warning" id="notification-version" aria-hidden="true" role="dialog" aria-labelledby="notification-warning-title" aria-describedby="notification-warning-description">
<div class="notification warning has-actions">
......@@ -404,10 +491,10 @@
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button save-button action-primary">Go to Newer Version</a>
<a href="#" class="action action-save action-primary">Go to Newer Version</a>
</li>
<li class="nav-item">
<a href="#" class="button cancel-button action-secondary">Continue Editing</a>
<a href="#" class="action action-cancel action-secondary">Continue Editing</a>
</li>
</ul>
</nav>
......@@ -428,10 +515,10 @@
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="action-primary">Yes, I want to Edit X</a>
<a href="#" class="action action-proceed action-primary">Yes, I want to Edit X</a>
</li>
<li class="nav-item">
<a href="#" class="action-secondary">No, I do not</a>
<a href="#" class="action action-cancel action-secondary">No, I do not</a>
</li>
</ul>
</nav>
......
......@@ -137,4 +137,4 @@ def clear_courses():
# $ mongo test_xmodule --eval "db.dropDatabase()"
_MODULESTORES = {}
modulestore().collection.drop()
update_templates()
update_templates(modulestore('direct'))
......@@ -8,15 +8,42 @@ import urllib
def fasthash(string):
m = hashlib.new("md4")
m.update(string)
return m.hexdigest()
"""
Hashes `string` into a string representation of a 128-bit digest.
"""
md4 = hashlib.new("md4")
md4.update(string)
return md4.hexdigest()
def cleaned_string(val):
"""
Converts `val` to unicode and URL-encodes special characters
(including quotes and spaces)
"""
return urllib.quote_plus(smart_str(val))
def safe_key(key, key_prefix, version):
safe_key = urllib.quote_plus(smart_str(key))
"""
Given a `key`, `key_prefix`, and `version`,
return a key that is safe to use with memcache.
`key`, `key_prefix`, and `version` can be numbers, strings, or unicode.
"""
# Clean for whitespace and control characters, which
# cause memcache to raise an exception
key = cleaned_string(key)
key_prefix = cleaned_string(key_prefix)
version = cleaned_string(version)
# Attempt to combine the prefix, version, and key
combined = ":".join([key_prefix, version, key])
if len(safe_key) > 250:
safe_key = fasthash(safe_key)
# If the total length is too long for memcache, hash it
if len(combined) > 250:
combined = fasthash(combined)
return ":".join([key_prefix, str(version), safe_key])
# Return the result
return combined
"""
Tests for memcache in util app
"""
from django.test import TestCase
from django.core.cache import get_cache
from django.conf import settings
from util.memcache import safe_key
class MemcacheTest(TestCase):
"""
Test memcache key cleanup
"""
# Test whitespace, control characters, and some non-ASCII UTF-16
UNICODE_CHAR_CODES = ([c for c in range(0, 30)] + [127] +
[129, 500, 2 ** 8 - 1, 2 ** 8 + 1, 2 ** 16 - 1])
def setUp(self):
self.cache = get_cache('default')
def test_safe_key(self):
key = safe_key('test', 'prefix', 'version')
self.assertEqual(key, 'prefix:version:test')
def test_numeric_inputs(self):
# Numeric key
self.assertEqual(safe_key(1, 'prefix', 'version'), 'prefix:version:1')
# Numeric prefix
self.assertEqual(safe_key('test', 5, 'version'), '5:version:test')
# Numeric version
self.assertEqual(safe_key('test', 'prefix', 5), 'prefix:5:test')
def test_safe_key_long(self):
# Choose lengths close to memcached's cutoff (250)
for length in [248, 249, 250, 251, 252]:
# Generate a key of that length
key = 'a' * length
# Make the key safe
key = safe_key(key, '', '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for key length {0}".format(length))
def test_long_key_prefix_version(self):
# Long key
key = safe_key('a' * 300, 'prefix', 'version')
self.assertTrue(self._is_valid_key(key))
# Long prefix
key = safe_key('key', 'a' * 300, 'version')
self.assertTrue(self._is_valid_key(key))
# Long version
key = safe_key('key', 'prefix', 'a' * 300)
self.assertTrue(self._is_valid_key(key))
def test_safe_key_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a key with that character
key = unichr(unicode_char)
# Make the key safe
key = safe_key(key, '', '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def test_safe_key_prefix_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a prefix with that character
prefix = unichr(unicode_char)
# Make the key safe
key = safe_key('test', prefix, '')
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def test_safe_key_version_unicode(self):
for unicode_char in self.UNICODE_CHAR_CODES:
# Generate a version with that character
version = unichr(unicode_char)
# Make the key safe
key = safe_key('test', '', version)
# The key should now be valid
self.assertTrue(self._is_valid_key(key),
msg="Failed for unicode character {0}".format(unicode_char))
def _is_valid_key(self, key):
"""
Test that a key is memcache-compatible.
Based on Django's validator in core.cache.backends.base
"""
# Check the length
if len(key) > 250:
return False
# Check that there are no spaces or control characters
for char in key:
if ord(char) < 33 or ord(char) == 127:
return False
return True
"""Tests for the util package"""
"""Tests for the Zendesk"""
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
......
......@@ -49,7 +49,10 @@ class _ZendeskApi(object):
settings.ZENDESK_USER,
settings.ZENDESK_API_KEY,
use_api_token=True,
api_version=2
api_version=2,
# As of 2012-05-08, Zendesk is using a CA that is not
# installed on our servers
client_args={"disable_ssl_certificate_validation": True}
)
def create_ticket(self, ticket):
......
......@@ -1783,7 +1783,7 @@ class FormulaResponse(LoncapaResponse):
response_tag = 'formularesponse'
hint_tag = 'formulahint'
allowed_inputfields = ['textline']
required_attributes = ['answer']
required_attributes = ['answer', 'samples']
max_inputfields = 1
def setup_response(self):
......
......@@ -52,6 +52,7 @@ setup(
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
],
......
......@@ -203,9 +203,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def save_instance_data(self):
for attribute in self.student_attributes:
child_attr = getattr(self.child_module, attribute)
if child_attr != getattr(self, attribute):
setattr(self, attribute, getattr(self.child_module, attribute))
setattr(self, attribute, getattr(self.child_module, attribute))
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
......
.input-cloud {
margin: 5px;
}
.result_cloud_section {
display: none;
width: 0px;
height: 0px;
}
.result_cloud_section.active {
display: block;
width: 635px;
height: auto;
margin-left: auto;
margin-right: auto;
}
.your_words{
font-size: 0.85em;
display: block;
}
\ No newline at end of file
......@@ -8,20 +8,23 @@ class @PeerGrading
@use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container')
@ajax_url = @peer_grading_container.data('ajax-url')
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
if @use_single_location.toLowerCase() == "true"
#If the peer grading element is linked to a single location, then activate the backend for that location
@activate_problem()
else
#Otherwise, activate the panel view.
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_list = $('.problem-list')
@construct_progress_bar()
@problem_button = $('.problem-button')
@problem_button.click @show_results
if @use_single_location
@activate_problem()
@problem_list = $('.problem-list')
@construct_progress_bar()
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()
......
/*
* Word cloud layout by Jason Davies
* https://github.com/jasondavies/d3-cloud
*
*
* Copyright (c) 2012, Jason Davies.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * The name Jason Davies may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL JASON DAVIES BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function(exports) {
function cloud() {
var size = [256, 256],
text = cloudText,
font = cloudFont,
fontSize = cloudFontSize,
fontStyle = cloudFontNormal,
fontWeight = cloudFontNormal,
rotate = cloudRotate,
padding = cloudPadding,
spiral = archimedeanSpiral,
words = [],
timeInterval = Infinity,
event = d3.dispatch("word", "end"),
timer = null,
cloud = {};
cloud.start = function() {
var board = zeroArray((size[0] >> 5) * size[1]),
bounds = null,
n = words.length,
i = -1,
tags = [],
data = words.map(function(d, i) {
d.text = text.call(this, d, i);
d.font = font.call(this, d, i);
d.style = fontStyle.call(this, d, i);
d.weight = fontWeight.call(this, d, i);
d.rotate = rotate.call(this, d, i);
d.size = ~~fontSize.call(this, d, i);
d.padding = cloudPadding.call(this, d, i);
return d;
}).sort(function(a, b) { return b.size - a.size; });
if (timer) clearInterval(timer);
timer = setInterval(step, 0);
step();
return cloud;
function step() {
var start = +new Date,
d;
while (+new Date - start < timeInterval && ++i < n && timer) {
d = data[i];
d.x = (size[0] * (Math.random() + .5)) >> 1;
d.y = (size[1] * (Math.random() + .5)) >> 1;
cloudSprite(d, data, i);
if (place(board, d, bounds)) {
tags.push(d);
event.word(d);
if (bounds) cloudBounds(bounds, d);
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
// Temporary hack
d.x -= size[0] >> 1;
d.y -= size[1] >> 1;
}
}
if (i >= n) {
cloud.stop();
event.end(tags, bounds);
}
}
}
cloud.stop = function() {
if (timer) {
clearInterval(timer);
timer = null;
}
return cloud;
};
cloud.timeInterval = function(x) {
if (!arguments.length) return timeInterval;
timeInterval = x == null ? Infinity : x;
return cloud;
};
function place(board, tag, bounds) {
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
startX = tag.x,
startY = tag.y,
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
s = spiral(size),
dt = Math.random() < .5 ? 1 : -1,
t = -dt,
dxdy,
dx,
dy;
while (dxdy = s(t += dt)) {
dx = ~~dxdy[0];
dy = ~~dxdy[1];
if (Math.min(dx, dy) > maxDelta) break;
tag.x = startX + dx;
tag.y = startY + dy;
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
// TODO only check for collisions within current bounds.
if (!bounds || !cloudCollide(tag, board, size[0])) {
if (!bounds || collideRects(tag, bounds)) {
var sprite = tag.sprite,
w = tag.width >> 5,
sw = size[0] >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
}
x += sw;
}
delete tag.sprite;
return true;
}
}
}
return false;
}
cloud.words = function(x) {
if (!arguments.length) return words;
words = x;
return cloud;
};
cloud.size = function(x) {
if (!arguments.length) return size;
size = [+x[0], +x[1]];
return cloud;
};
cloud.font = function(x) {
if (!arguments.length) return font;
font = d3.functor(x);
return cloud;
};
cloud.fontStyle = function(x) {
if (!arguments.length) return fontStyle;
fontStyle = d3.functor(x);
return cloud;
};
cloud.fontWeight = function(x) {
if (!arguments.length) return fontWeight;
fontWeight = d3.functor(x);
return cloud;
};
cloud.rotate = function(x) {
if (!arguments.length) return rotate;
rotate = d3.functor(x);
return cloud;
};
cloud.text = function(x) {
if (!arguments.length) return text;
text = d3.functor(x);
return cloud;
};
cloud.spiral = function(x) {
if (!arguments.length) return spiral;
spiral = spirals[x + ""] || x;
return cloud;
};
cloud.fontSize = function(x) {
if (!arguments.length) return fontSize;
fontSize = d3.functor(x);
return cloud;
};
cloud.padding = function(x) {
if (!arguments.length) return padding;
padding = d3.functor(x);
return cloud;
};
return d3.rebind(cloud, event, "on");
}
function cloudText(d) {
return d.text;
}
function cloudFont() {
return "serif";
}
function cloudFontNormal() {
return "normal";
}
function cloudFontSize(d) {
return Math.sqrt(d.value);
}
function cloudRotate() {
return (~~(Math.random() * 6) - 3) * 30;
}
function cloudPadding() {
return 1;
}
// Fetches a monochrome sprite bitmap for the specified text.
// Load in batches for speed.
function cloudSprite(d, data, di) {
if (d.sprite) return;
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
var x = 0,
y = 0,
maxh = 0,
n = data.length;
di--;
while (++di < n) {
d = data[di];
c.save();
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
var w = c.measureText(d.text + "m").width * ratio,
h = d.size << 1;
if (d.rotate) {
var sr = Math.sin(d.rotate * cloudRadians),
cr = Math.cos(d.rotate * cloudRadians),
wcr = w * cr,
wsr = w * sr,
hcr = h * cr,
hsr = h * sr;
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
} else {
w = (w + 0x1f) >> 5 << 5;
}
if (h > maxh) maxh = h;
if (x + w >= (cw << 5)) {
x = 0;
y += maxh;
maxh = 0;
}
if (y + h >= ch) break;
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
if (d.rotate) c.rotate(d.rotate * cloudRadians);
c.fillText(d.text, 0, 0);
c.restore();
d.width = w;
d.height = h;
d.xoff = x;
d.yoff = y;
d.x1 = w >> 1;
d.y1 = h >> 1;
d.x0 = -d.x1;
d.y0 = -d.y1;
x += w;
}
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
sprite = [];
while (--di >= 0) {
d = data[di];
var w = d.width,
w32 = w >> 5,
h = d.y1 - d.y0,
p = d.padding;
// Zero the buffer
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
x = d.xoff;
if (x == null) return;
y = d.yoff;
var seen = 0,
seenRow = -1;
for (var j = 0; j < h; j++) {
for (var i = 0; i < w; i++) {
var k = w32 * j + (i >> 5),
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
if (p) {
if (j) sprite[k - w32] |= m;
if (j < w - 1) sprite[k + w32] |= m;
m |= (m << 1) | (m >> 1);
}
sprite[k] |= m;
seen |= m;
}
if (seen) seenRow = j;
else {
d.y0++;
h--;
j--;
y++;
}
}
d.y1 = d.y0 + seenRow;
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
}
}
// Use mask-based collision detection.
function cloudCollide(tag, board, sw) {
sw >>= 5;
var sprite = tag.sprite,
w = tag.width >> 5,
lx = tag.x - (w << 4),
sx = lx & 0x7f,
msx = 32 - sx,
h = tag.y1 - tag.y0,
x = (tag.y + tag.y0) * sw + (lx >> 5),
last;
for (var j = 0; j < h; j++) {
last = 0;
for (var i = 0; i <= w; i++) {
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
& board[x + i]) return true;
}
x += sw;
}
return false;
}
function cloudBounds(bounds, d) {
var b0 = bounds[0],
b1 = bounds[1];
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
}
function collideRects(a, b) {
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
}
function archimedeanSpiral(size) {
var e = size[0] / size[1];
return function(t) {
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
};
}
function rectangularSpiral(size) {
var dy = 4,
dx = dy * size[0] / size[1],
x = 0,
y = 0;
return function(t) {
var sign = t < 0 ? -1 : 1;
// See triangular numbers: T_n = n * (n + 1) / 2.
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
case 0: x += dx; break;
case 1: y += dy; break;
case 2: x -= dx; break;
default: y -= dy; break;
}
return [x, y];
};
}
// TODO reuse arrays?
function zeroArray(n) {
var a = [],
i = -1;
while (++i < n) a[i] = 0;
return a;
}
var cloudRadians = Math.PI / 180,
cw = 1 << 11 >> 5,
ch = 1 << 11,
canvas,
ratio = 1;
if (typeof document !== "undefined") {
canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
canvas.width = (cw << 5) / ratio;
canvas.height = ch / ratio;
} else {
// node-canvas support
var Canvas = require("canvas");
canvas = new Canvas(cw << 5, ch);
}
var c = canvas.getContext("2d"),
spirals = {
archimedean: archimedeanSpiral,
rectangular: rectangularSpiral
};
c.fillStyle = "red";
c.textAlign = "center";
exports.cloud = cloud;
})(typeof exports === "undefined" ? d3.layout || (d3.layout = {}) : exports);
This source diff could not be displayed because it is too large. You can view the blob instead.
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('logme', [], function () {
var debugMode;
// debugMode can be one of the following:
//
// true - All messages passed to logme will be written to the internal
// browser console.
// false - Suppress all output to the internal browser console.
//
// Obviously, if anywhere there is a direct console.log() call, we can't do
// anything about it. That's why use logme() - it will allow to turn off
// the output of debug information with a single change to a variable.
debugMode = true;
return logme;
/*
* function: logme
*
* A helper function that provides logging facilities. We don't want
* to call console.log() directly, because sometimes it is not supported
* by the browser. Also when everything is routed through this function.
* the logging output can be easily turned off.
*
* logme() supports multiple parameters. Each parameter will be passed to
* console.log() function separately.
*
*/
function logme() {
var i;
if (
(typeof debugMode === 'undefined') ||
(debugMode !== true) ||
(typeof window.console === 'undefined')
) {
return;
}
for (i = 0; i < arguments.length; i++) {
window.console.log(arguments[i]);
}
} // End-of: function logme
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
window.WordCloud = function (el) {
RequireJS.require(['WordCloudMain'], function (WordCloudMain) {
new WordCloudMain(el);
});
};
/**
* @file The main module definition for Word Cloud XModule.
*
* Defines a constructor function which operates on a DOM element. Either show the user text inputs so
* he can enter words, or render his selected words along with the word cloud representing the top words.
*
* @module WordCloudMain
*
* @exports WordCloudMain
*
* @requires logme
*
* @external d3, $, RequireJS
*/
(function (requirejs, require, define) {
define('WordCloudMain', ['logme'], function (logme) {
/**
* @function WordCloudMain
*
* This function will process all the attributes from the DOM element passed, taking all of
* the configuration attributes. It will either then attach a callback handler for the click
* event on the button in the case when the user needs to enter words, or it will call the
* appropriate mehtod to generate and render a word cloud from user's enetered words along with
* all of the other words.
*
* @constructor
*
* @param {jQuery} el DOM element where the word cloud will be processed and created.
*/
var WordCloudMain = function (el) {
var _this = this;
this.wordCloudEl = $(el).find('.word_cloud');
// Get the URL to which we will post the users words.
this.ajax_url = this.wordCloudEl.data('ajax-url');
// Dimensions of the box where the word cloud will be drawn.
this.width = 635;
this.height = 635;
// Hide WordCloud container before Ajax request done
this.wordCloudEl.hide();
// Retriveing response from the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/' + 'get_state', null,
function (response) {
if (response.status !== 'success') {
logme('ERROR: ' + response.error);
return;
}
_this.configJson = response;
}
)
.done(function () {
// Show WordCloud container after Ajax request done
_this.wordCloudEl.show();
if (_this.configJson && _this.configJson.submitted) {
_this.showWordCloud(_this.configJson);
return;
}
});
$(el).find('input.save').on('click', function () {
_this.submitAnswer();
});
}; // End-of: var WordCloudMain = function (el) {
/**
* @function submitAnswer
*
* Callback to be executed when the user eneter his words. It will send user entries to the
* server, and upon receiving correct response, will call the function to generate the
* word cloud.
*/
WordCloudMain.prototype.submitAnswer = function () {
var _this = this,
data = {'student_words': []};
// Populate the data to be sent to the server with user's words.
this.wordCloudEl.find('input.input-cloud').each(function (index, value) {
data.student_words.push($(value).val());
});
// Send the data to the server as an AJAX request. Attach a callback that will
// be fired on server's response.
$.postWithPrefix(
_this.ajax_url + '/' + 'submit', $.param(data),
function (response) {
if (response.status !== 'success') {
logme('ERROR: ' + response.error);
return;
}
_this.showWordCloud(response);
}
);
}; // End-of: WordCloudMain.prototype.submitAnswer = function () {
/**
* @function showWordCloud
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* This function will set up everything for d3 and launch the draw method. Among other things,
* iw will determine maximum word size.
*/
WordCloudMain.prototype.showWordCloud = function (response) {
var words,
_this = this,
maxSize, minSize, scaleFactor, maxFontSize, minFontSize;
this.wordCloudEl.find('.input_cloud_section').hide();
words = response.top_words;
maxSize = 0;
minSize = 10000;
scaleFactor = 1;
maxFontSize = 200;
minFontSize = 15;
// Find the word with the maximum percentage. I.e. the most popular word.
$.each(words, function (index, word) {
if (word.size > maxSize) {
maxSize = word.size;
}
if (word.size < minSize) {
minSize = word.size;
}
});
// Find the longest word, and calculate the scale appropriately. This is
// required so that even long words fit into the drawing area.
//
// This is a fix for: if the word is very long and/or big, it is discarded by
// for unknown reason.
$.each(words, function (index, word) {
var tempScaleFactor = 1.0,
size = ((word.size / maxSize) * maxFontSize);
if (size * 0.7 * word.text.length > _this.width) {
tempScaleFactor = ((_this.width / word.text.length) / 0.7) / size;
}
if (scaleFactor > tempScaleFactor) {
scaleFactor = tempScaleFactor;
}
});
// Update the maximum font size based on the longest word.
maxFontSize *= scaleFactor;
// Generate the word cloud.
d3.layout.cloud().size([this.width, this.height])
.words(words)
.rotate(function () {
return Math.floor((Math.random() * 2) * 90);
})
.font('Impact')
.fontSize(function (d) {
var size = (d.size / maxSize) * maxFontSize;
size = size >= minFontSize ? size : minFontSize;
return size;
})
.on('end', function (words, bounds) {
// Draw the word cloud.
_this.drawWordCloud(response, words, bounds);
})
.start();
}; // End-of: WordCloudMain.prototype.showWordCloud = function (response) {
/**
* @function drawWordCloud
*
* This function will be called when d3 has finished initing the state for our word cloud,
* and it is ready to hand off the process to the drawing routine. Basically set up everything
* necessary for the actual drwing of the words.
*
* @param {object} response The response from the server that contains the user's entered words
* along with all of the top words.
*
* @param {array} words An array of objects. Each object must have two properties. One property
* is 'text' (the actual word), and the other property is 'size' which represents the number that the
* word was enetered by the students.
*
* @param {array} bounds An array of two objects. First object is the top-left coordinates of the bounding
* box where all of the words fir, second object is the bottom-right coordinates of the bounding box. Each
* coordinate object contains two properties: 'x', and 'y'.
*/
WordCloudMain.prototype.drawWordCloud = function (response, words, bounds) {
// Color words in different colors.
var fill = d3.scale.category20(),
// Will be populated by words the user enetered.
studentWordsKeys = [],
// Comma separated string of user enetered words.
studentWordsStr,
// By default we do not scale.
scale = 1,
// Caсhing of DOM element
cloudSectionEl = this.wordCloudEl.find('.result_cloud_section'),
// Needed for caсhing of d3 group elements
groupEl;
// If bounding rectangle is given, scale based on the bounding box of all the words.
if (bounds) {
scale = 0.5 * Math.min(
this.width / Math.abs(bounds[1].x - this.width / 2),
this.width / Math.abs(bounds[0].x - this.width / 2),
this.height / Math.abs(bounds[1].y - this.height / 2),
this.height / Math.abs(bounds[0].y - this.height / 2)
);
}
$.each(response.student_words, function (word, stat) {
var percent = (response.display_student_percents) ? ' ' + (Math.round(100 * (stat / response.total_count))) + '%' : '';
studentWordsKeys.push('<strong>' + word + '</strong>' + percent);
});
studentWordsStr = '' + studentWordsKeys.join(', ');
cloudSectionEl
.addClass('active')
.find('.your_words').html(studentWordsStr)
.find('.total_num_words').html(response.total_count);
$(cloudSectionEl.attr('id') + ' .word_cloud').empty();
// Actual drawing of word cloud.
groupEl = d3.select('#' + cloudSectionEl.attr('id') + ' .word_cloud').append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', 'translate(' + (0.5 * this.width) + ',' + (0.5 * this.height) + ')')
.selectAll('text')
.data(words)
.enter().append('g');
groupEl
.append('title')
.text(function (d) {
var res = '';
$.each(response.top_words, function(index, value){
if (value.text === d.text) {
res = value.percent + '%';
return;
}
});
return res;
});
groupEl
.append('text')
.style('font-size', function (d) {
return d.size + 'px';
})
.style('font-family', 'Impact')
.style('fill', function (d, i) {
return fill(i);
})
.attr('text-anchor', 'middle')
.attr('transform', function (d) {
return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')scale(' + scale + ')';
})
.text(function (d) {
return d.text;
});
}; // End-of: WordCloudMain.prototype.drawWordCloud = function (words, bounds) {
return WordCloudMain;
}); // End-of: define('WordCloudMain', ['logme'], function (logme) {
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
......@@ -476,7 +476,15 @@ class MongoModuleStore(ModuleStoreBase):
'''
# TODO (vshnayder): Why do I have to specify i4x here?
course_filter = Location("i4x", category="course")
return self.get_items(course_filter)
return [
course
for course
in self.get_items(course_filter)
if not (
course.location.org == 'edx' and
course.location.course == 'templates'
)
]
def _find_one(self, location):
'''Look for a given location in the collection. If revision is not
......
......@@ -42,7 +42,7 @@ class ModuleStoreTestCase(TestCase):
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
update_templates()
update_templates(modulestore)
@classmethod
def setUpClass(cls):
......
......@@ -7,6 +7,7 @@ from pprint import pprint
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from .test_modulestore import check_path_to_location
from . import DATA_DIR
......@@ -45,6 +46,7 @@ class TestMongoModuleStore(object):
# Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses)
update_templates(store)
return store
@staticmethod
......@@ -103,3 +105,11 @@ class TestMongoModuleStore(object):
def test_path_to_location(self):
'''Make sure that path_to_location works'''
check_path_to_location(self.store)
def test_get_courses_has_no_templates(self):
courses = self.store.get_courses()
for course in courses:
assert_false(
course.location.org == 'edx' and course.location.course == 'templates',
'{0} is a template course'.format(course)
)
......@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.fields import Date, StringyFloat
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
......@@ -28,14 +28,14 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object):
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state)
......@@ -93,9 +93,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/"
if not isinstance(self.max_grade, (int, long)):
#This could result in an exception, but not wrapping in a try catch block so it moves up the stack
self.max_grade = int(self.max_grade)
#StringyInteger could return None, so keep this check.
if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.")
def closed(self):
return self._closed(self.timeinfo)
......
......@@ -4,8 +4,6 @@ to do set of polls.
On the client side we show:
If student does not yet anwered - Question with set of choices.
If student have answered - Question with statistics for each answers.
Student can't change his answer.
"""
import cgi
......
......@@ -19,7 +19,6 @@ from collections import defaultdict
from .x_module import XModuleDescriptor
from .mako_module import MakoDescriptorSystem
from .modulestore import Location
from .modulestore.django import modulestore
log = logging.getLogger(__name__)
......@@ -50,7 +49,7 @@ class TemplateTestSystem(MakoDescriptorSystem):
)
def update_templates():
def update_templates(modulestore):
"""
Updates the set of templates in the modulestore with all templates currently
available from the installed plugins
......@@ -58,7 +57,7 @@ def update_templates():
# cdodge: build up a list of all existing templates. This will be used to determine which
# templates have been removed from disk - and thus we need to remove from the DB
templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None])
templates_to_delete = modulestore.get_items(['i4x', 'edx', 'templates', None, None, None])
for category, templates in all_templates().items():
for template in templates:
......@@ -86,9 +85,9 @@ def update_templates():
), exc_info=True)
continue
modulestore('direct').update_item(template_location, template.data)
modulestore('direct').update_children(template_location, template.children)
modulestore('direct').update_metadata(template_location, template.metadata)
modulestore.update_item(template_location, template.data)
modulestore.update_children(template_location, template.children)
modulestore.update_metadata(template_location, template.metadata)
# remove template from list of templates to delete
templates_to_delete = [t for t in templates_to_delete if t.location != template_location]
......@@ -97,4 +96,4 @@ def update_templates():
if len(templates_to_delete) > 0:
logging.debug('deleting dangling templates = {0}'.format(templates_to_delete))
for template in templates_to_delete:
modulestore('direct').delete_item(template.location)
modulestore.delete_item(template.location)
---
metadata:
display_name: Word cloud
version: 1
num_inputs: 5
num_top_words: 250
display_student_percents: True
data: {}
children: []
import unittest
from fs.osfs import OSFS
from nose.tools import assert_equals, assert_true
from path import path
from tempfile import mkdtemp
import shutil
......@@ -22,9 +21,9 @@ def strip_filenames(descriptor):
"""
Recursively strips 'filename' from all children's definitions.
"""
print "strip filename from {desc}".format(desc=descriptor.location.url())
print("strip filename from {desc}".format(desc=descriptor.location.url()))
descriptor._model_data.pop('filename', None)
if hasattr(descriptor, 'xml_attributes'):
if 'filename' in descriptor.xml_attributes:
del descriptor.xml_attributes['filename']
......@@ -41,12 +40,12 @@ class RoundTripTestCase(unittest.TestCase):
'''
def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(self.temp_dir)
print "Copying test course to temp dir {0}".format(root_dir)
print("Copying test course to temp dir {0}".format(root_dir))
data_dir = path(data_dir)
shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import"
print("Starting import")
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses = initial_import.get_courses()
......@@ -55,7 +54,7 @@ class RoundTripTestCase(unittest.TestCase):
# export to the same directory--that way things like the custom_tags/ folder
# will still be there.
print "Starting export"
print("Starting export")
fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir)
......@@ -63,14 +62,14 @@ class RoundTripTestCase(unittest.TestCase):
with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
print "Starting second import"
print("Starting second import")
second_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1)
exported_course = courses2[0]
print "Checking course equality"
print("Checking course equality")
# HACK: filenames change when changing file formats
# during imports from old-style courses. Ignore them.
......@@ -81,16 +80,18 @@ class RoundTripTestCase(unittest.TestCase):
self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id
print "Checking key equality"
print("Checking key equality")
self.assertEquals(sorted(initial_import.modules[course_id].keys()),
sorted(second_import.modules[course_id].keys()))
print "Checking module equality"
print("Checking module equality")
for location in initial_import.modules[course_id].keys():
print "Checking", location
print("Checking", location)
if location.category == 'html':
print ("Skipping html modules--they can't import in"
" final form without writing files...")
print(
"Skipping html modules--they can't import in"
" final form without writing files..."
)
continue
self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location])
......@@ -123,3 +124,6 @@ class RoundTripTestCase(unittest.TestCase):
def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
def test_word_cloud_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "word_cloud")
......@@ -53,7 +53,7 @@ class BaseCourseTestCase(unittest.TestCase):
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
print("Importing {0}".format(name))
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
......@@ -145,7 +145,7 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(start_xml)
compute_inherited_metadata(descriptor)
print descriptor, descriptor._model_data
print(descriptor, descriptor._model_data)
self.assertEqual(descriptor.lms.due, Date().from_json(v))
# Check that the child inherits due correctly
......@@ -161,7 +161,7 @@ class ImportTestCase(BaseCourseTestCase):
exported_xml = descriptor.export_to_xml(resource_fs)
# Check that the exported xml is just a pointer
print "Exported xml:", exported_xml
print("Exported xml:", exported_xml)
pointer = etree.fromstring(exported_xml)
self.assertTrue(is_pointer_tag(pointer))
# but it's a special case course pointer
......@@ -255,29 +255,29 @@ class ImportTestCase(BaseCourseTestCase):
no = ["""<html url_name="blah" also="this"/>""",
"""<html url_name="blah">some text</html>""",
"""<problem url_name="blah"><sub>tree</sub></problem>""",
"""<course org="HogwartsX" course="Mathemagics" url_name="3.14159">
"""<problem url_name="blah"><sub>tree</sub></problem>""",
"""<course org="HogwartsX" course="Mathemagics" url_name="3.14159">
<chapter>3</chapter>
</course>
"""]
"""]
for xml_str in yes:
print "should be True for {0}".format(xml_str)
print("should be True for {0}".format(xml_str))
self.assertTrue(is_pointer_tag(etree.fromstring(xml_str)))
for xml_str in no:
print "should be False for {0}".format(xml_str)
print("should be False for {0}".format(xml_str))
self.assertFalse(is_pointer_tag(etree.fromstring(xml_str)))
def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly"""
print "Starting import"
print("Starting import")
course = self.get_course('toy')
def check_for_key(key, node):
"recursive check for presence of key"
print "Checking {0}".format(node.location.url())
print("Checking {0}".format(node.location.url()))
self.assertTrue(key in node._model_data)
for c in node.get_children():
check_for_key(key, c)
......@@ -322,14 +322,14 @@ class ImportTestCase(BaseCourseTestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8")
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9")
def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly"""
print "Starting import"
print("Starting import")
# Not using get_courses because we need the modulestore object too afterward
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
courses = modulestore.get_courses()
......@@ -337,10 +337,10 @@ class ImportTestCase(BaseCourseTestCase):
course = courses[0]
course_id = course.id
print "course errors:"
print("course errors:")
for (msg, err) in modulestore.get_item_errors(course.location):
print msg
print err
print(msg)
print(err)
chapters = course.get_children()
self.assertEquals(len(chapters), 2)
......@@ -348,12 +348,12 @@ class ImportTestCase(BaseCourseTestCase):
ch2 = chapters[1]
self.assertEquals(ch2.url_name, "secret:magic")
print "Ch2 location: ", ch2.location
print("Ch2 location: ", ch2.location)
also_ch2 = modulestore.get_instance(course_id, ch2.location)
self.assertEquals(ch2, also_ch2)
print "making sure html loaded"
print("making sure html loaded")
cloc = course.location
loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab')
html = modulestore.get_instance(course_id, loc)
......@@ -378,11 +378,11 @@ class ImportTestCase(BaseCourseTestCase):
for i in (2, 3):
video = sections[i]
# Name should be 'video_{hash}'
print "video {0} url_name: {1}".format(i, video.url_name)
print("video {0} url_name: {1}".format(i, video.url_name))
self.assertEqual(len(video.url_name), len('video_') + 12)
def test_poll_and_conditional_xmodule(self):
def test_poll_and_conditional_import(self):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['conditional_and_poll'])
course = modulestore.get_courses()[0]
......@@ -393,10 +393,31 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(len(sections), 1)
location = course.location
location = Location(location.tag, location.org, location.course,
'sequential', 'Problem_Demos')
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.children), 2)
conditional_location = Location(
location.tag, location.org, location.course,
'conditional', 'condone'
)
module = modulestore.get_instance(course.id, conditional_location)
self.assertEqual(len(module.children), 1)
poll_location = Location(
location.tag, location.org, location.course,
'poll_question', 'first_poll'
)
module = modulestore.get_instance(course.id, poll_location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.voted, False)
self.assertEqual(module.poll_answer, '')
self.assertEqual(module.poll_answers, {})
self.assertEqual(
module.answers,
[
{'text': u'Yes', 'id': 'Yes'},
{'text': u'No', 'id': 'No'},
{'text': u"Don't know", 'id': 'Dont_know'}
]
)
def test_error_on_import(self):
'''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorModule'''
......@@ -406,7 +427,6 @@ class ImportTestCase(BaseCourseTestCase):
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
def test_graphicslidertool_import(self):
'''
Check to see if definition_from_xml in gst_module.py
......@@ -423,6 +443,26 @@ class ImportTestCase(BaseCourseTestCase):
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml)
def test_word_cloud_import(self):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud'])
course = modulestore.get_courses()[0]
chapters = course.get_children()
ch1 = chapters[0]
sections = ch1.get_children()
self.assertEqual(len(sections), 1)
location = course.location
location = Location(
location.tag, location.org, location.course,
'word_cloud', 'cloud1'
)
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5')
self.assertEqual(module.num_top_words, '250')
def test_cohort_config(self):
"""
Check that cohort config parsing works right.
......
# -*- coding: utf-8 -*-
"""Test for Xmodule functional logic."""
import json
import unittest
from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
class PostData:
"""Class which emulate postdata."""
def __init__(self, dict_data):
self.dict_data = dict_data
def getlist(self, key):
return self.dict_data.get(key)
class LogicTest(unittest.TestCase):
......@@ -13,15 +24,18 @@ class LogicTest(unittest.TestCase):
raw_model_data = {}
def setUp(self):
class EmptyClass: pass
class EmptyClass:
pass
self.system = None
self.location = None
self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class
self.xmodule = self.xmodule_class(self.system, self.location,
self.descriptor, self.raw_model_data)
self.xmodule = self.xmodule_class(
self.system, self.location,
self.descriptor, self.raw_model_data
)
def ajax_request(self, dispatch, get):
return json.loads(self.xmodule.handle_ajax(dispatch, get))
......@@ -64,3 +78,42 @@ class ConditionalModuleTest(LogicTest):
html = response['html']
self.assertEqual(html, [])
class WordCloudModuleTest(LogicTest):
descriptor_class = WordCloudDescriptor
raw_model_data = {
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
'submitted': False
}
def test_bad_ajax_request(self):
# TODO: move top global test. Formalize all our Xmodule errors.
response = self.ajax_request('bad_dispatch', {})
self.assertDictEqual(response, {
'status': 'fail',
'error': 'Unknown Command!'
})
def test_good_ajax_request(self):
post_data = PostData({'student_words[]': ['cat', 'cat', 'dog', 'sun']})
response = self.ajax_request('submit', post_data)
self.assertEqual(response['status'], 'success')
self.assertEqual(response['submitted'], True)
self.assertEqual(response['total_count'], 22)
self.assertDictEqual(
response['student_words'],
{'sun': 1, 'dog': 6, 'cat': 12}
)
self.assertListEqual(
response['top_words'],
[{'text': 'dad', 'size': 2, 'percent': 9.0},
{'text': 'sun', 'size': 1, 'percent': 5.0},
{'text': 'dog', 'size': 6, 'percent': 27.0},
{'text': 'mom', 'size': 1, 'percent': 5.0},
{'text': 'cat', 'size': 12, 'percent': 54.0}]
)
self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) )
"""Word cloud is ungraded xblock used by students to
generate and view word cloud.
On the client side we show:
If student does not yet anwered - `num_inputs` numbers of text inputs.
If student have answered - words he entered and cloud.
"""
import json
import logging
from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from xmodule.x_module import XModule
from xblock.core import Scope, String, Object, Boolean, List, Integer
log = logging.getLogger(__name__)
def pretty_bool(value):
BOOL_DICT = [True, "True", "true", "T", "t", "1"]
return value in BOOL_DICT
class WordCloudFields(object):
"""XFields for word cloud."""
display_name = String(
help="Display name for this module",
scope=Scope.settings
)
num_inputs = Integer(
help="Number of inputs.",
scope=Scope.settings,
default=5
)
num_top_words = Integer(
help="Number of max words, which will be displayed.",
scope=Scope.settings,
default=250
)
display_student_percents = Boolean(
help="Display usage percents for each word?",
scope=Scope.settings,
default=True
)
# Fields for descriptor.
submitted = Boolean(
help="Whether this student has posted words to the cloud.",
scope=Scope.user_state,
default=False
)
student_words = List(
help="Student answer.",
scope=Scope.user_state,
default=[]
)
all_words = Object(
help="All possible words from all students.",
scope=Scope.content
)
top_words = Object(
help="Top num_top_words words for word cloud.",
scope=Scope.content
)
class WordCloudModule(WordCloudFields, XModule):
"""WordCloud Xmodule"""
js = {
'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
'js': [resource_string(__name__, 'js/src/word_cloud/logme.js'),
resource_string(__name__, 'js/src/word_cloud/d3.min.js'),
resource_string(__name__, 'js/src/word_cloud/d3.layout.cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
}
css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
js_module_name = "WordCloud"
def get_state(self):
"""Return success json answer for client."""
if self.submitted:
total_count = sum(self.all_words.itervalues())
return json.dumps({
'status': 'success',
'submitted': True,
'display_student_percents': pretty_bool(
self.display_student_percents
),
'student_words': {
word: self.all_words[word] for word in self.student_words
},
'total_count': total_count,
'top_words': self.prepare_words(self.top_words, total_count)
})
else:
return json.dumps({
'status': 'success',
'submitted': False,
'display_student_percents': False,
'student_words': {},
'total_count': 0,
'top_words': {}
})
def good_word(self, word):
"""Convert raw word to suitable word."""
return word.strip().lower()
def prepare_words(self, top_words, total_count):
"""Convert words dictionary for client API.
:param top_words: Top words dictionary
:type top_words: dict
:param total_count: Total number of words
:type total_count: int
:rtype: list of dicts. Every dict is 3 keys: text - actual word,
size - counter of word, percent - percent in top_words dataset.
Calculates corrected percents for every top word:
For every word except last, it calculates rounded percent.
For the last is 100 - sum of all other percents.
"""
list_to_return = []
percents = 0
for num, word_tuple in enumerate(top_words.iteritems()):
if num == len(top_words) - 1:
percent = 100 - percents
else:
percent = round(100.0 * word_tuple[1] / total_count)
percents += percent
list_to_return.append(
{
'text': word_tuple[0],
'size': word_tuple[1],
'percent': percent
}
)
return list_to_return
def top_dict(self, dict_obj, amount):
"""Return top words from all words, filtered by number of
occurences
:param dict_obj: all words
:type dict_obj: dict
:param amount: number of words to be in top dict
:type amount: int
:rtype: dict
"""
return dict(
sorted(
dict_obj.items(),
key=lambda x: x[1],
reverse=True
)[:amount]
)
def handle_ajax(self, dispatch, post):
"""Ajax handler.
Args:
dispatch: string request slug
post: dict request get parameters
Returns:
json string
"""
if dispatch == 'submit':
if self.submitted:
return json.dumps({
'status': 'fail',
'error': 'You have already posted your data.'
})
# Student words from client.
# FIXME: we must use raw JSON, not a post data (multipart/form-data)
raw_student_words = post.getlist('student_words[]')
student_words = filter(None, map(self.good_word, raw_student_words))
self.student_words = student_words
# FIXME: fix this, when xblock will support mutable types.
# Now we use this hack.
# speed issues
temp_all_words = self.all_words
self.submitted = True
# Save in all_words.
for word in self.student_words:
temp_all_words[word] = temp_all_words.get(word, 0) + 1
# Update top_words.
self.top_words = self.top_dict(
temp_all_words,
int(self.num_top_words)
)
# Save all_words in database.
self.all_words = temp_all_words
return self.get_state()
elif dispatch == 'get_state':
return self.get_state()
else:
return json.dumps({
'status': 'fail',
'error': 'Unknown Command!'
})
def get_html(self):
"""Template rendering."""
context = {
'element_id': self.location.html_id(),
'element_class': self.location.category,
'ajax_url': self.system.ajax_url,
'num_inputs': int(self.num_inputs),
'submitted': self.submitted
}
self.content = self.system.render_template('word_cloud.html', context)
return self.content
class WordCloudDescriptor(WordCloudFields, RawDescriptor):
"""Descriptor for WordCloud Xmodule."""
module_class = WordCloudModule
template_dir_name = 'word_cloud'
stores_state = True
mako_template = "widgets/raw-edit.html"
// Backbone.js 0.9.10
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var n=this,B=n.Backbone,h=[],C=h.push,u=h.slice,D=h.splice,g;g="undefined"!==typeof exports?exports:n.Backbone={};g.VERSION="0.9.10";var f=n._;!f&&"undefined"!==typeof require&&(f=require("underscore"));g.$=n.jQuery||n.Zepto||n.ender;g.noConflict=function(){n.Backbone=B;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var v=/\s+/,q=function(a,b,c,d){if(!c)return!0;if("object"===typeof c)for(var e in c)a[b].apply(a,[e,c[e]].concat(d));else if(v.test(c)){c=c.split(v);e=0;for(var f=c.length;e<
f;e++)a[b].apply(a,[c[e]].concat(d))}else return!0},w=function(a,b){var c,d=-1,e=a.length;switch(b.length){case 0:for(;++d<e;)(c=a[d]).callback.call(c.ctx);break;case 1:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0]);break;case 2:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1]);break;case 3:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1],b[2]);break;default:for(;++d<e;)(c=a[d]).callback.apply(c.ctx,b)}},h=g.Events={on:function(a,b,c){if(!q(this,"on",a,[b,c])||!b)return this;this._events||(this._events=
{});(this._events[a]||(this._events[a]=[])).push({callback:b,context:c,ctx:c||this});return this},once:function(a,b,c){if(!q(this,"once",a,[b,c])||!b)return this;var d=this,e=f.once(function(){d.off(a,e);b.apply(this,arguments)});e._callback=b;this.on(a,e,c);return this},off:function(a,b,c){var d,e,t,g,j,l,k,h;if(!this._events||!q(this,"off",a,[b,c]))return this;if(!a&&!b&&!c)return this._events={},this;g=a?[a]:f.keys(this._events);j=0;for(l=g.length;j<l;j++)if(a=g[j],d=this._events[a]){t=[];if(b||
c){k=0;for(h=d.length;k<h;k++)e=d[k],(b&&b!==e.callback&&b!==e.callback._callback||c&&c!==e.context)&&t.push(e)}this._events[a]=t}return this},trigger:function(a){if(!this._events)return this;var b=u.call(arguments,1);if(!q(this,"trigger",a,b))return this;var c=this._events[a],d=this._events.all;c&&w(c,b);d&&w(d,arguments);return this},listenTo:function(a,b,c){var d=this._listeners||(this._listeners={}),e=a._listenerId||(a._listenerId=f.uniqueId("l"));d[e]=a;a.on(b,"object"===typeof b?this:c,this);
return this},stopListening:function(a,b,c){var d=this._listeners;if(d){if(a)a.off(b,"object"===typeof b?this:c,this),!b&&!c&&delete d[a._listenerId];else{"object"===typeof b&&(c=this);for(var e in d)d[e].off(b,c,this);this._listeners={}}return this}}};h.bind=h.on;h.unbind=h.off;f.extend(g,h);var r=g.Model=function(a,b){var c,d=a||{};this.cid=f.uniqueId("c");this.attributes={};b&&b.collection&&(this.collection=b.collection);b&&b.parse&&(d=this.parse(d,b)||{});if(c=f.result(this,"defaults"))d=f.defaults({},
d,c);this.set(d,b);this.changed={};this.initialize.apply(this,arguments)};f.extend(r.prototype,h,{changed:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},sync:function(){return g.sync.apply(this,arguments)},get:function(a){return this.attributes[a]},escape:function(a){return f.escape(this.get(a))},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e,g,p,j,l,k;if(null==a)return this;"object"===typeof a?(e=a,c=b):(e={})[a]=b;c||(c={});
if(!this._validate(e,c))return!1;g=c.unset;p=c.silent;a=[];j=this._changing;this._changing=!0;j||(this._previousAttributes=f.clone(this.attributes),this.changed={});k=this.attributes;l=this._previousAttributes;this.idAttribute in e&&(this.id=e[this.idAttribute]);for(d in e)b=e[d],f.isEqual(k[d],b)||a.push(d),f.isEqual(l[d],b)?delete this.changed[d]:this.changed[d]=b,g?delete k[d]:k[d]=b;if(!p){a.length&&(this._pending=!0);b=0;for(d=a.length;b<d;b++)this.trigger("change:"+a[b],this,k[a[b]],c)}if(j)return this;
if(!p)for(;this._pending;)this._pending=!1,this.trigger("change",this,c);this._changing=this._pending=!1;return this},unset:function(a,b){return this.set(a,void 0,f.extend({},b,{unset:!0}))},clear:function(a){var b={},c;for(c in this.attributes)b[c]=void 0;return this.set(b,f.extend({},a,{unset:!0}))},hasChanged:function(a){return null==a?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._changing?
this._previousAttributes:this.attributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return null==a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){if(!a.set(a.parse(d,e),e))return!1;b&&b(a,d,e)};return this.sync("read",this,a)},save:function(a,b,c){var d,e,g=this.attributes;
null==a||"object"===typeof a?(d=a,c=b):(d={})[a]=b;if(d&&(!c||!c.wait)&&!this.set(d,c))return!1;c=f.extend({validate:!0},c);if(!this._validate(d,c))return!1;d&&c.wait&&(this.attributes=f.extend({},g,d));void 0===c.parse&&(c.parse=!0);e=c.success;c.success=function(a,b,c){a.attributes=g;var k=a.parse(b,c);c.wait&&(k=f.extend(d||{},k));if(f.isObject(k)&&!a.set(k,c))return!1;e&&e(a,b,c)};a=this.isNew()?"create":c.patch?"patch":"update";"patch"===a&&(c.attrs=d);a=this.sync(a,this,c);d&&c.wait&&(this.attributes=
g);return a},destroy:function(a){a=a?f.clone(a):{};var b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};a.success=function(a,b,e){(e.wait||a.isNew())&&d();c&&c(a,b,e)};if(this.isNew())return a.success(this,null,a),!1;var e=this.sync("delete",this,a);a.wait||d();return e},url:function(){var a=f.result(this,"urlRoot")||f.result(this.collection,"url")||x();return this.isNew()?a:a+("/"===a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},
isNew:function(){return null==this.id},isValid:function(a){return!this.validate||!this.validate(this.attributes,a)},_validate:function(a,b){if(!b.validate||!this.validate)return!0;a=f.extend({},this.attributes,a);var c=this.validationError=this.validate(a,b)||null;if(!c)return!0;this.trigger("invalid",this,c,b||{});return!1}});var s=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);void 0!==b.comparator&&(this.comparator=b.comparator);this.models=[];this._reset();this.initialize.apply(this,
arguments);a&&this.reset(a,f.extend({silent:!0},b))};f.extend(s.prototype,h,{model:r,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return g.sync.apply(this,arguments)},add:function(a,b){a=f.isArray(a)?a.slice():[a];b||(b={});var c,d,e,g,p,j,l,k,h,m;l=[];k=b.at;h=this.comparator&&null==k&&!1!=b.sort;m=f.isString(this.comparator)?this.comparator:null;c=0;for(d=a.length;c<d;c++)(e=this._prepareModel(g=a[c],b))?(p=this.get(e))?b.merge&&(p.set(g===
e?e.attributes:g,b),h&&(!j&&p.hasChanged(m))&&(j=!0)):(l.push(e),e.on("all",this._onModelEvent,this),this._byId[e.cid]=e,null!=e.id&&(this._byId[e.id]=e)):this.trigger("invalid",this,g,b);l.length&&(h&&(j=!0),this.length+=l.length,null!=k?D.apply(this.models,[k,0].concat(l)):C.apply(this.models,l));j&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=l.length;c<d;c++)(e=l[c]).trigger("add",e,this,b);j&&this.trigger("sort",this,b);return this},remove:function(a,b){a=f.isArray(a)?a.slice():[a];
b||(b={});var c,d,e,g;c=0;for(d=a.length;c<d;c++)if(g=this.get(a[c]))delete this._byId[g.id],delete this._byId[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:this.length},b));return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},
b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},slice:function(a,b){return this.models.slice(a,b)},get:function(a){if(null!=a)return this._idAttr||(this._idAttr=this.model.prototype.idAttribute),this._byId[a.id||a.cid||a[this._idAttr]||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){if(!this.comparator)throw Error("Cannot sort a set without a comparator");
a||(a={});f.isString(this.comparator)||1===this.comparator.length?this.models=this.sortBy(this.comparator,this):this.models.sort(f.bind(this.comparator,this));a.silent||this.trigger("sort",this,a);return this},pluck:function(a){return f.invoke(this.models,"get",a)},update:function(a,b){b=f.extend({add:!0,merge:!0,remove:!0},b);b.parse&&(a=this.parse(a,b));var c,d,e,g,h=[],j=[],l={};f.isArray(a)||(a=a?[a]:[]);if(b.add&&!b.remove)return this.add(a,b);d=0;for(e=a.length;d<e;d++)c=a[d],g=this.get(c),
b.remove&&g&&(l[g.cid]=!0),(b.add&&!g||b.merge&&g)&&h.push(c);if(b.remove){d=0;for(e=this.models.length;d<e;d++)c=this.models[d],l[c.cid]||j.push(c)}j.length&&this.remove(j,b);h.length&&this.add(h,b);return this},reset:function(a,b){b||(b={});b.parse&&(a=this.parse(a,b));for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);b.previousModels=this.models.slice();this._reset();a&&this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=
a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){a[e.update?"update":"reset"](d,e);b&&b(a,d,e)};return this.sync("read",this,a)},create:function(a,b){b=b?f.clone(b):{};if(!(a=this._prepareModel(a,b)))return!1;b.wait||this.add(a,b);var c=this,d=b.success;b.success=function(a,b,f){f.wait&&c.add(a,f);d&&d(a,b,f)};a.save(null,b);return a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models.length=
0;this._byId={}},_prepareModel:function(a,b){if(a instanceof r)return a.collection||(a.collection=this),a;b||(b={});b.collection=this;var c=new this.model(a,b);return!c._validate(a,b)?!1:c},_removeReference:function(a){this===a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"===a||"remove"===a)&&c!==this||("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=
b)),this.trigger.apply(this,arguments))},sortedIndex:function(a,b,c){b||(b=this.comparator);var d=f.isFunction(b)?b:function(a){return a.get(b)};return f.sortedIndex(this.models,a,d,c)}});f.each("forEach each map collect reduce foldl inject reduceRight foldr find detect filter select reject every all some any include contains invoke max min toArray size first head take initial rest tail drop last without indexOf shuffle lastIndexOf isEmpty chain".split(" "),function(a){s.prototype[a]=function(){var b=
u.call(arguments);b.unshift(this.models);return f[a].apply(f,b)}});f.each(["groupBy","countBy","sortBy"],function(a){s.prototype[a]=function(b,c){var d=f.isFunction(b)?b:function(a){return a.get(b)};return f[a](this.models,d,c)}});var y=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},E=/\((.*?)\)/g,F=/(\(\?)?:\w+/g,G=/\*\w+/g,H=/[\-{}\[\]+?.,\\\^$|#\s]/g;f.extend(y.prototype,h,{initialize:function(){},route:function(a,b,c){f.isRegExp(a)||
(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));this.trigger("route",b,d);g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b);return this},_bindRoutes:function(){if(this.routes)for(var a,b=f.keys(this.routes);null!=(a=b.pop());)this.route(a,this.routes[a])},_routeToRegExp:function(a){a=a.replace(H,"\\$&").replace(E,"(?:$1)?").replace(F,
function(a,c){return c?a:"([^/]+)"}).replace(G,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl");"undefined"!==typeof window&&(this.location=window.location,this.history=window.history)},z=/^[#\/]|\s+$/g,I=/^\/+|\/+$/g,J=/msie [\w.]+/,K=/\/$/;m.started=!1;f.extend(m.prototype,h,{interval:50,getHash:function(a){return(a=(a||this).location.href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,
b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){a=this.location.pathname;var c=this.root.replace(K,"");a.indexOf(c)||(a=a.substr(c.length))}else a=this.getHash();return a.replace(z,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this.root=this.options.root;this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||
!this.history||!this.history.pushState);a=this.getFragment();var b=document.documentMode,b=J.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b);this.root=("/"+this.root+"/").replace(I,"/");b&&this._wantsHashChange&&(this.iframe=g.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a));if(this._hasPushState)g.$(window).on("popstate",this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)g.$(window).on("hashchange",this.checkUrl);
else this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval));this.fragment=a;a=this.location;b=a.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),this.location.replace(this.root+this.location.search+"#"+this.fragment),!0;this._wantsPushState&&(this._hasPushState&&b&&a.hash)&&(this.fragment=this.getHash().replace(z,""),this.history.replaceState({},document.title,
this.root+this.fragment+a.search));if(!this.options.silent)return this.loadUrl()},stop:function(){g.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a===this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a===this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},
loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};a=this.getFragment(a||"");if(this.fragment!==a){this.fragment=a;var c=this.root+a;if(this._hasPushState)this.history[b.replace?"replaceState":"pushState"]({},document.title,c);else if(this._wantsHashChange)this._updateHash(this.location,a,b.replace),this.iframe&&a!==this.getFragment(this.getHash(this.iframe))&&
(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,a,b.replace));else return this.location.assign(c);b.trigger&&this.loadUrl(a)}},_updateHash:function(a,b,c){c?(c=a.href.replace(/(javascript:|#).*$/,""),a.replace(c+"#"+b)):a.hash="#"+b}});g.history=new m;var A=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},L=/^(\S+)\s*(.*)$/,M="model collection el id attributes className tagName events".split(" ");
f.extend(A.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof g.$?a:g.$(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=f.result(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);
if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(L),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);if(""===d)this.$el.on(e,c);else this.$el.on(e,d,c)}}},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},f.result(this,"options"),a));f.extend(this,f.pick(a,M));this.options=a},_ensureElement:function(){if(this.el)this.setElement(f.result(this,"el"),!1);else{var a=f.extend({},f.result(this,"attributes"));
this.id&&(a.id=f.result(this,"id"));this.className&&(a["class"]=f.result(this,"className"));a=g.$("<"+f.result(this,"tagName")+">").attr(a);this.setElement(a,!1)}}});var N={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=N[a];f.defaults(c||(c={}),{emulateHTTP:g.emulateHTTP,emulateJSON:g.emulateJSON});var e={type:d,dataType:"json"};c.url||(e.url=f.result(b,"url")||x());if(null==c.data&&b&&("create"===a||"update"===a||"patch"===a))e.contentType="application/json",
e.data=JSON.stringify(c.attrs||b.toJSON(c));c.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(c.emulateHTTP&&("PUT"===d||"DELETE"===d||"PATCH"===d)){e.type="POST";c.emulateJSON&&(e.data._method=d);var h=c.beforeSend;c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d);if(h)return h.apply(this,arguments)}}"GET"!==e.type&&!c.emulateJSON&&(e.processData=!1);var m=c.success;c.success=function(a){m&&m(b,a,c);b.trigger("sync",b,a,c)};
var j=c.error;c.error=function(a){j&&j(b,a,c);b.trigger("error",b,a,c)};a=c.xhr=g.ajax(f.extend(e,c));b.trigger("request",b,a,c);return a};g.ajax=function(){return g.$.ajax.apply(g.$,arguments)};r.extend=s.extend=y.extend=A.extend=m.extend=function(a,b){var c=this,d;d=a&&f.has(a,"constructor")?a.constructor:function(){return c.apply(this,arguments)};f.extend(d,c,b);var e=function(){this.constructor=d};e.prototype=c.prototype;d.prototype=new e;a&&f.extend(d.prototype,a);d.__super__=c.prototype;return d};
var x=function(){throw Error('A "url" property or function must be specified');}}).call(this);
(function(){var t=this;var e=t.Backbone;var i=[];var r=i.push;var s=i.slice;var n=i.splice;var a;if(typeof exports!=="undefined"){a=exports}else{a=t.Backbone={}}a.VERSION="1.0.0";var h=t._;if(!h&&typeof require!=="undefined")h=require("underscore");a.$=t.jQuery||t.Zepto||t.ender||t.$;a.noConflict=function(){t.Backbone=e;return this};a.emulateHTTP=false;a.emulateJSON=false;var o=a.Events={on:function(t,e,i){if(!l(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,i){if(!l(this,"once",t,[e,i])||!e)return this;var r=this;var s=h.once(function(){r.off(t,s);e.apply(this,arguments)});s._callback=e;return this.on(t,s,i)},off:function(t,e,i){var r,s,n,a,o,u,c,f;if(!this._events||!l(this,"off",t,[e,i]))return this;if(!t&&!e&&!i){this._events={};return this}a=t?[t]:h.keys(this._events);for(o=0,u=a.length;o<u;o++){t=a[o];if(n=this._events[t]){this._events[t]=r=[];if(e||i){for(c=0,f=n.length;c<f;c++){s=n[c];if(e&&e!==s.callback&&e!==s.callback._callback||i&&i!==s.context){r.push(s)}}}if(!r.length)delete this._events[t]}}return this},trigger:function(t){if(!this._events)return this;var e=s.call(arguments,1);if(!l(this,"trigger",t,e))return this;var i=this._events[t];var r=this._events.all;if(i)c(i,e);if(r)c(r,arguments);return this},stopListening:function(t,e,i){var r=this._listeners;if(!r)return this;var s=!e&&!i;if(typeof e==="object")i=this;if(t)(r={})[t._listenerId]=t;for(var n in r){r[n].off(e,i,this);if(s)delete this._listeners[n]}return this}};var u=/\s+/;var l=function(t,e,i,r){if(!i)return true;if(typeof i==="object"){for(var s in i){t[e].apply(t,[s,i[s]].concat(r))}return false}if(u.test(i)){var n=i.split(u);for(var a=0,h=n.length;a<h;a++){t[e].apply(t,[n[a]].concat(r))}return false}return true};var c=function(t,e){var i,r=-1,s=t.length,n=e[0],a=e[1],h=e[2];switch(e.length){case 0:while(++r<s)(i=t[r]).callback.call(i.ctx);return;case 1:while(++r<s)(i=t[r]).callback.call(i.ctx,n);return;case 2:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a);return;case 3:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a,h);return;default:while(++r<s)(i=t[r]).callback.apply(i.ctx,e)}};var f={listenTo:"on",listenToOnce:"once"};h.each(f,function(t,e){o[e]=function(e,i,r){var s=this._listeners||(this._listeners={});var n=e._listenerId||(e._listenerId=h.uniqueId("l"));s[n]=e;if(typeof i==="object")r=this;e[t](i,r,this);return this}});o.bind=o.on;o.unbind=o.off;h.extend(a,o);var d=a.Model=function(t,e){var i;var r=t||{};e||(e={});this.cid=h.uniqueId("c");this.attributes={};h.extend(this,h.pick(e,p));if(e.parse)r=this.parse(r,e)||{};if(i=h.result(this,"defaults")){r=h.defaults({},r,i)}this.set(r,e);this.changed={};this.initialize.apply(this,arguments)};var p=["url","urlRoot","collection"];h.extend(d.prototype,o,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(t){return h.clone(this.attributes)},sync:function(){return a.sync.apply(this,arguments)},get:function(t){return this.attributes[t]},escape:function(t){return h.escape(this.get(t))},has:function(t){return this.get(t)!=null},set:function(t,e,i){var r,s,n,a,o,u,l,c;if(t==null)return this;if(typeof t==="object"){s=t;i=e}else{(s={})[t]=e}i||(i={});if(!this._validate(s,i))return false;n=i.unset;o=i.silent;a=[];u=this._changing;this._changing=true;if(!u){this._previousAttributes=h.clone(this.attributes);this.changed={}}c=this.attributes,l=this._previousAttributes;if(this.idAttribute in s)this.id=s[this.idAttribute];for(r in s){e=s[r];if(!h.isEqual(c[r],e))a.push(r);if(!h.isEqual(l[r],e)){this.changed[r]=e}else{delete this.changed[r]}n?delete c[r]:c[r]=e}if(!o){if(a.length)this._pending=true;for(var f=0,d=a.length;f<d;f++){this.trigger("change:"+a[f],this,c[a[f]],i)}}if(u)return this;if(!o){while(this._pending){this._pending=false;this.trigger("change",this,i)}}this._pending=false;this._changing=false;return this},unset:function(t,e){return this.set(t,void 0,h.extend({},e,{unset:true}))},clear:function(t){var e={};for(var i in this.attributes)e[i]=void 0;return this.set(e,h.extend({},t,{unset:true}))},hasChanged:function(t){if(t==null)return!h.isEmpty(this.changed);return h.has(this.changed,t)},changedAttributes:function(t){if(!t)return this.hasChanged()?h.clone(this.changed):false;var e,i=false;var r=this._changing?this._previousAttributes:this.attributes;for(var s in t){if(h.isEqual(r[s],e=t[s]))continue;(i||(i={}))[s]=e}return i},previous:function(t){if(t==null||!this._previousAttributes)return null;return this._previousAttributes[t]},previousAttributes:function(){return h.clone(this._previousAttributes)},fetch:function(t){t=t?h.clone(t):{};if(t.parse===void 0)t.parse=true;var e=this;var i=t.success;t.success=function(r){if(!e.set(e.parse(r,t),t))return false;if(i)i(e,r,t);e.trigger("sync",e,r,t)};R(this,t);return this.sync("read",this,t)},save:function(t,e,i){var r,s,n,a=this.attributes;if(t==null||typeof t==="object"){r=t;i=e}else{(r={})[t]=e}if(r&&(!i||!i.wait)&&!this.set(r,i))return false;i=h.extend({validate:true},i);if(!this._validate(r,i))return false;if(r&&i.wait){this.attributes=h.extend({},a,r)}if(i.parse===void 0)i.parse=true;var o=this;var u=i.success;i.success=function(t){o.attributes=a;var e=o.parse(t,i);if(i.wait)e=h.extend(r||{},e);if(h.isObject(e)&&!o.set(e,i)){return false}if(u)u(o,t,i);o.trigger("sync",o,t,i)};R(this,i);s=this.isNew()?"create":i.patch?"patch":"update";if(s==="patch")i.attrs=r;n=this.sync(s,this,i);if(r&&i.wait)this.attributes=a;return n},destroy:function(t){t=t?h.clone(t):{};var e=this;var i=t.success;var r=function(){e.trigger("destroy",e,e.collection,t)};t.success=function(s){if(t.wait||e.isNew())r();if(i)i(e,s,t);if(!e.isNew())e.trigger("sync",e,s,t)};if(this.isNew()){t.success();return false}R(this,t);var s=this.sync("delete",this,t);if(!t.wait)r();return s},url:function(){var t=h.result(this,"urlRoot")||h.result(this.collection,"url")||U();if(this.isNew())return t;return t+(t.charAt(t.length-1)==="/"?"":"/")+encodeURIComponent(this.id)},parse:function(t,e){return t},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return this.id==null},isValid:function(t){return this._validate({},h.extend(t||{},{validate:true}))},_validate:function(t,e){if(!e.validate||!this.validate)return true;t=h.extend({},this.attributes,t);var i=this.validationError=this.validate(t,e)||null;if(!i)return true;this.trigger("invalid",this,i,h.extend(e||{},{validationError:i}));return false}});var v=["keys","values","pairs","invert","pick","omit"];h.each(v,function(t){d.prototype[t]=function(){var e=s.call(arguments);e.unshift(this.attributes);return h[t].apply(h,e)}});var g=a.Collection=function(t,e){e||(e={});if(e.url)this.url=e.url;if(e.model)this.model=e.model;if(e.comparator!==void 0)this.comparator=e.comparator;this._reset();this.initialize.apply(this,arguments);if(t)this.reset(t,h.extend({silent:true},e))};var m={add:true,remove:true,merge:true};var y={add:true,merge:false,remove:false};h.extend(g.prototype,o,{model:d,initialize:function(){},toJSON:function(t){return this.map(function(e){return e.toJSON(t)})},sync:function(){return a.sync.apply(this,arguments)},add:function(t,e){return this.set(t,h.defaults(e||{},y))},remove:function(t,e){t=h.isArray(t)?t.slice():[t];e||(e={});var i,r,s,n;for(i=0,r=t.length;i<r;i++){n=this.get(t[i]);if(!n)continue;delete this._byId[n.id];delete this._byId[n.cid];s=this.indexOf(n);this.models.splice(s,1);this.length--;if(!e.silent){e.index=s;n.trigger("remove",n,this,e)}this._removeReference(n)}return this},set:function(t,e){e=h.defaults(e||{},m);if(e.parse)t=this.parse(t,e);if(!h.isArray(t))t=t?[t]:[];var i,s,a,o,u,l;var c=e.at;var f=this.comparator&&c==null&&e.sort!==false;var d=h.isString(this.comparator)?this.comparator:null;var p=[],v=[],g={};for(i=0,s=t.length;i<s;i++){if(!(a=this._prepareModel(t[i],e)))continue;if(u=this.get(a)){if(e.remove)g[u.cid]=true;if(e.merge){u.set(a.attributes,e);if(f&&!l&&u.hasChanged(d))l=true}}else if(e.add){p.push(a);a.on("all",this._onModelEvent,this);this._byId[a.cid]=a;if(a.id!=null)this._byId[a.id]=a}}if(e.remove){for(i=0,s=this.length;i<s;++i){if(!g[(a=this.models[i]).cid])v.push(a)}if(v.length)this.remove(v,e)}if(p.length){if(f)l=true;this.length+=p.length;if(c!=null){n.apply(this.models,[c,0].concat(p))}else{r.apply(this.models,p)}}if(l)this.sort({silent:true});if(e.silent)return this;for(i=0,s=p.length;i<s;i++){(a=p[i]).trigger("add",a,this,e)}if(l)this.trigger("sort",this,e);return this},reset:function(t,e){e||(e={});for(var i=0,r=this.models.length;i<r;i++){this._removeReference(this.models[i])}e.previousModels=this.models;this._reset();this.add(t,h.extend({silent:true},e));if(!e.silent)this.trigger("reset",this,e);return this},push:function(t,e){t=this._prepareModel(t,e);this.add(t,h.extend({at:this.length},e));return t},pop:function(t){var e=this.at(this.length-1);this.remove(e,t);return e},unshift:function(t,e){t=this._prepareModel(t,e);this.add(t,h.extend({at:0},e));return t},shift:function(t){var e=this.at(0);this.remove(e,t);return e},slice:function(t,e){return this.models.slice(t,e)},get:function(t){if(t==null)return void 0;return this._byId[t.id!=null?t.id:t.cid||t]},at:function(t){return this.models[t]},where:function(t,e){if(h.isEmpty(t))return e?void 0:[];return this[e?"find":"filter"](function(e){for(var i in t){if(t[i]!==e.get(i))return false}return true})},findWhere:function(t){return this.where(t,true)},sort:function(t){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");t||(t={});if(h.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(h.bind(this.comparator,this))}if(!t.silent)this.trigger("sort",this,t);return this},sortedIndex:function(t,e,i){e||(e=this.comparator);var r=h.isFunction(e)?e:function(t){return t.get(e)};return h.sortedIndex(this.models,t,r,i)},pluck:function(t){return h.invoke(this.models,"get",t)},fetch:function(t){t=t?h.clone(t):{};if(t.parse===void 0)t.parse=true;var e=t.success;var i=this;t.success=function(r){var s=t.reset?"reset":"set";i[s](r,t);if(e)e(i,r,t);i.trigger("sync",i,r,t)};R(this,t);return this.sync("read",this,t)},create:function(t,e){e=e?h.clone(e):{};if(!(t=this._prepareModel(t,e)))return false;if(!e.wait)this.add(t,e);var i=this;var r=e.success;e.success=function(s){if(e.wait)i.add(t,e);if(r)r(t,s,e)};t.save(null,e);return t},parse:function(t,e){return t},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(t,e){if(t instanceof d){if(!t.collection)t.collection=this;return t}e||(e={});e.collection=this;var i=new this.model(t,e);if(!i._validate(t,e)){this.trigger("invalid",this,t,e);return false}return i},_removeReference:function(t){if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(e&&t==="change:"+e.idAttribute){delete this._byId[e.previous(e.idAttribute)];if(e.id!=null)this._byId[e.id]=e}this.trigger.apply(this,arguments)}});var _=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","indexOf","shuffle","lastIndexOf","isEmpty","chain"];h.each(_,function(t){g.prototype[t]=function(){var e=s.call(arguments);e.unshift(this.models);return h[t].apply(h,e)}});var w=["groupBy","countBy","sortBy"];h.each(w,function(t){g.prototype[t]=function(e,i){var r=h.isFunction(e)?e:function(t){return t.get(e)};return h[t](this.models,r,i)}});var b=a.View=function(t){this.cid=h.uniqueId("view");this._configure(t||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var x=/^(\S+)\s*(.*)$/;var E=["model","collection","el","id","attributes","className","tagName","events"];h.extend(b.prototype,o,{tagName:"div",$:function(t){return this.$el.find(t)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(t,e){if(this.$el)this.undelegateEvents();this.$el=t instanceof a.$?t:a.$(t);this.el=this.$el[0];if(e!==false)this.delegateEvents();return this},delegateEvents:function(t){if(!(t||(t=h.result(this,"events"))))return this;this.undelegateEvents();for(var e in t){var i=t[e];if(!h.isFunction(i))i=this[t[e]];if(!i)continue;var r=e.match(x);var s=r[1],n=r[2];i=h.bind(i,this);s+=".delegateEvents"+this.cid;if(n===""){this.$el.on(s,i)}else{this.$el.on(s,n,i)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_configure:function(t){if(this.options)t=h.extend({},h.result(this,"options"),t);h.extend(this,h.pick(t,E));this.options=t},_ensureElement:function(){if(!this.el){var t=h.extend({},h.result(this,"attributes"));if(this.id)t.id=h.result(this,"id");if(this.className)t["class"]=h.result(this,"className");var e=a.$("<"+h.result(this,"tagName")+">").attr(t);this.setElement(e,false)}else{this.setElement(h.result(this,"el"),false)}}});a.sync=function(t,e,i){var r=k[t];h.defaults(i||(i={}),{emulateHTTP:a.emulateHTTP,emulateJSON:a.emulateJSON});var s={type:r,dataType:"json"};if(!i.url){s.url=h.result(e,"url")||U()}if(i.data==null&&e&&(t==="create"||t==="update"||t==="patch")){s.contentType="application/json";s.data=JSON.stringify(i.attrs||e.toJSON(i))}if(i.emulateJSON){s.contentType="application/x-www-form-urlencoded";s.data=s.data?{model:s.data}:{}}if(i.emulateHTTP&&(r==="PUT"||r==="DELETE"||r==="PATCH")){s.type="POST";if(i.emulateJSON)s.data._method=r;var n=i.beforeSend;i.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",r);if(n)return n.apply(this,arguments)}}if(s.type!=="GET"&&!i.emulateJSON){s.processData=false}if(s.type==="PATCH"&&window.ActiveXObject&&!(window.external&&window.external.msActiveXFilteringEnabled)){s.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var o=i.xhr=a.ajax(h.extend(s,i));e.trigger("request",e,o,i);return o};var k={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};a.ajax=function(){return a.$.ajax.apply(a.$,arguments)};var S=a.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var $=/\((.*?)\)/g;var T=/(\(\?)?:\w+/g;var H=/\*\w+/g;var A=/[\-{}\[\]+?.,\\\^$|#\s]/g;h.extend(S.prototype,o,{initialize:function(){},route:function(t,e,i){if(!h.isRegExp(t))t=this._routeToRegExp(t);if(h.isFunction(e)){i=e;e=""}if(!i)i=this[e];var r=this;a.history.route(t,function(s){var n=r._extractParameters(t,s);i&&i.apply(r,n);r.trigger.apply(r,["route:"+e].concat(n));r.trigger("route",e,n);a.history.trigger("route",r,e,n)});return this},navigate:function(t,e){a.history.navigate(t,e);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=h.result(this,"routes");var t,e=h.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(A,"\\$&").replace($,"(?:$1)?").replace(T,function(t,e){return e?t:"([^/]+)"}).replace(H,"(.*?)");return new RegExp("^"+t+"$")},_extractParameters:function(t,e){var i=t.exec(e).slice(1);return h.map(i,function(t){return t?decodeURIComponent(t):null})}});var I=a.History=function(){this.handlers=[];h.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var N=/^[#\/]|\s+$/g;var P=/^\/+|\/+$/g;var O=/msie [\w.]+/;var C=/\/$/;I.started=false;h.extend(I.prototype,o,{interval:50,getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=this.location.pathname;var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.substr(i.length)}else{t=this.getHash()}}return t.replace(N,"")},start:function(t){if(I.started)throw new Error("Backbone.history has already been started");I.started=true;this.options=h.extend({},{root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var e=this.getFragment();var i=document.documentMode;var r=O.exec(navigator.userAgent.toLowerCase())&&(!i||i<=7);this.root=("/"+this.root+"/").replace(P,"/");if(r&&this._wantsHashChange){this.iframe=a.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;this.navigate(e)}if(this._hasPushState){a.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!r){a.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=e;var s=this.location;var n=s.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!n){this.fragment=this.getFragment(null,true);this.location.replace(this.root+this.location.search+"#"+this.fragment);return true}else if(this._wantsPushState&&this._hasPushState&&n&&s.hash){this.fragment=this.getHash().replace(N,"");this.history.replaceState({},document.title,this.root+this.fragment+s.search)}if(!this.options.silent)return this.loadUrl()},stop:function(){a.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);I.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(t){var e=this.fragment=this.getFragment(t);var i=h.any(this.handlers,function(t){if(t.route.test(e)){t.callback(e);return true}});return i},navigate:function(t,e){if(!I.started)return false;if(!e||e===true)e={trigger:e};t=this.getFragment(t||"");if(this.fragment===t)return;this.fragment=t;var i=this.root+t;if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});a.history=new I;var j=function(t,e){var i=this;var r;if(t&&h.has(t,"constructor")){r=t.constructor}else{r=function(){return i.apply(this,arguments)}}h.extend(r,i,e);var s=function(){this.constructor=r};s.prototype=i.prototype;r.prototype=new s;if(t)h.extend(r.prototype,t);r.__super__=i.prototype;return r};d.extend=g.extend=S.extend=b.extend=I.extend=j;var U=function(){throw new Error('A "url" property or function must be specified')};var R=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}}}).call(this);
/*
//@ sourceMappingURL=backbone-min.map
*/
(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.index<t.index?-1:1}),"value")};var k=function(n,t,r,e){var u={},i=F(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return k(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return k(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:F(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","/":"&#x2F;"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
\ No newline at end of file
(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index<t.index?-1:1}),"value")};var F=function(n,t,r,e){var u={},i=k(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return F(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return F(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","/":"&#x2F;"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
\ No newline at end of file
Any place that says "YEAR_SEMESTER" needs to be replaced with something
in the form "2013_Spring". Take note of this name exactly, you'll need to
use it everywhere, precisely - capitalization is very important.
See https://github.com/MITx/mitx/blob/master/doc/xml-format.md for more on all this.
-----------------------
about/: Files that live here will be visible OUTSIDE OF COURSEWARE.
YEAR_SEMESTER/
end_date.html: Specifies in plain-text the end date of the course
overview.html: Text of the overview of the course
short_description.html: 10-15 words about the course
prerequisites.html: Any prerequisites for the course, or None if there are none.
course/
YEAR_SEMESTER.xml: This is your top-level xml page that points at chapters.
Can just be <course/> for now.
course.xml: This top level file points at a file in roots/. See creating_course.xml.
creating_course.xml: Explains how to create course.xml
info/: Files that live here will be visible on the COURSE LANDING PAGE
(Course Info) WITHIN THE COURSEWARE.
YEAR_SEMESTER/
handouts.html: A list of handouts, or an empty file if there are none
(if this file doesn't exist, it displays an error)
updates.html: Course updates.
policies/
YEAR_SEMESTER/
policy.json: See https://github.com/MITx/mitx/blob/master/doc/xml-format.md
for more on the fields specified by this file.
grading_policy.json: Optional -- you don't need it to get a course off the
ground but will eventually. For more info see
https://github.com/MITx/mitx/blob/master/doc/course_grading.md
roots/
YEAR_SEMESTER.xml: Looks something like
<course url_name="YEAR_SEMESTER" org="ORG" course="COURSENUM"/>
where ORG in {"MITx", "HarvardX", "BerkeleyX"}
static/
See README.
images/
course_image.jpg: You MUST have an image named this to be the background
banner image on edx.org
-----------------------
\ No newline at end of file
content-harvard-justicex
========================
\ No newline at end of file
<section class="about">
<h2>About ER22x</h2>
<p>Justice is a critical analysis of classical and contemporary theories of justice, including discussion of present-day applications. Topics include affirmative action, income distribution, same-sex marriage, the role of markets, debates about rights (human rights and property rights), arguments for and against equality, dilemmas of loyalty in public and private life. The course invites students to subject their own views on these controversies to critical examination.</p>
<p>The principle readings for the course are texts by Aristotle, John Locke, Immanuel Kant, John Stuart Mill, and John Rawls. Other assigned readings include writings by contemporary philosophers, court cases, and articles about political controversies that raise philosophical questions.</p>
<!--
<p>The assigned readings will be freely available online. They are also collected in an edited volume, <emph>Justice: A Reader</emph> (ed. Michael Sandel, Oxford University Press). Students who would like further guidance on the themes of the lectures can read Michael Sandel, <emph>Justice: What’s the Right Thing to Do?</emph> (Recommended but not required.)
</p>
-->
</section>
<section class="course-staff">
<h2>Course instructor</h3>
<article class="teacher">
<!-- TODO: Need to change image location -->
<!-- First Professor -->
<div class="teacher-image"><img src="/static/images/professor-sandel.jpg"/></div>
<h3>Michael J. Sandel</h3>
<p>Michael J. Sandel is the Anne T. and Robert M. Bass Professor of Government at Harvard University, where he teaches political philosophy.  His course "Justice" has enrolled more than 15,000 Harvard students.  Sandel's writings have been published in 21 languages.  His books include <i>What Money Can't Buy: The Moral Limits of Markets</i> (2012); <i>Justice: What's the Right Thing to Do?</i> (2009); <i>The Case against Perfection: Ethics in the Age of Genetic Engineering</i> (2007); <i>Public Philosophy: Essays on Morality in Politics</i> (2005); <i>Democracy's Discontent</i> (1996); and <i>Liberalism and the Limits of Justice</i>(1982; 2nd ed., 1998). </p>
<p><br></p>
</section>
<section class="faq">
<section class="responses">
<h2>Frequently Asked Questions</h2>
<article class="response">
<h3>How much does it cost to take the course?</h3>
<p>Nothing! The course is free.</p>
</article>
<article class="response">
<h3>Does the course have any prerequisites?</h3>
<p>No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.</p>
</article>
<article class="response">
<h3>Do I need any other materials to take the course?</h3>
<p>No. As long as you’ve got a computer to access the website, you are ready to take the course.</p>
</article>
<article class="response">
<h3>Is there a textbook for the course?</h3>
<p>All of the course readings that are in the public domain are freely available online, at links provided on the course website. The course can be taken using these free resources alone. For those who wish to purchase a printed version of the assigned readings, an edited volume entitled, Justice: A Reader (ed., Michael Sandel) is available in paperback from Oxford University Press (in bookstores and from online booksellers). Those who would like supplementary readings on the themes of the lectures can find them in Michael Sandel's book Justice: What's the Right Thing to Do?, which is available in various languages throughout the world. This book is not required, and the course can be taken using the free online resources alone.</p>
</article>
<article class="response">
<h3>Do I need to watch the lectures at a specific time?</h3>
<p>No. You can watch the lectures at your leisure.</p>
</article>
<article class="response">
<h3>Will I be able to participate in class discussions?</h3>
<p>Yes, in several ways: </p>
<ol>
<li><p> Each lecture invites you to respond to a poll question related to the themes of the lecture. If you respond to the question, you will be presented with a challenge to the opinion you have expressed, and invited to reply to the challenge. You can also, if you wish, comment on the opinions and responses posted by other students in the course, continuing the discussion.</p></li>
<li><p> In addition to the poll question, each class contains a discussion prompt that invites you to offer your view on a controversial question related to the lecture. If you wish, you can respond to this question, and then see what other students have to say about the argument you present. You can also comment on the opinions posted by other students. One aim of the course is to promote reasoned public dialogue about hard moral and political questions. </p></li>
<li><p> Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.</p></li>
</ol>
</article>
<article class="response">
<h3>Will certificates be awarded?</h3>
<p>Yes. Online learners who achieve a passing grade in a course can earn a certificate of mastery. These certificates will indicate you have successfully completed the course, but will not include a specific grade. Certificates will be issued by edX under the name of HarvardX, designating the institution from which the course originated. </p>
</article>
</section>
</section>
\ No newline at end of file
JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
<iframe width="560" height="315" src="http://www.youtube.com/embed/fajlZMdPkKE#!" frameborder="0" allowfullscreen></iframe>
\ No newline at end of file
<chapter>
<sequential url_name="Problem_Demos"/>
</chapter>
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
<!-- Name this file eg "2012_Fall.xml" or "2013_Spring.xml"
Take note of this name exactly, you'll need to use it everywhere. -->
<course>
<chapter url_name="Staff"/>
</course>
<!-- A file named "course.xml" in your top-level should point
at the appropriate roots file. You can do so like this:
$ rm course.xml
$ ln -s roots/YEAR_SEMESTER.xml course.xml
Ask Sarina for help with this. -->
<ol>
<li>A list of course handouts, or an empty file if there are none.</li>
</ol>
<!-- If you wish to make a welcome announcement -->
<ol>
<li><h2>December 9</h2>
<section class="update-description">
<p>Announcement text</p>
</section>
</li>
</ol>
{
"course/2013_Spring": {
"start": "2099-01-01T00:00",
"advertised_start" : "Spring 2013",
"display_name": "Justice"
}
}
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
<sequential>
<vertical name="test_vertical">
<word_cloud name="cloud1" display_name="cloud" num_inputs="5" num_top_words="250" />
</vertical>
</sequential>
Images, handouts, and other statically-served content should go ONLY
in this directory.
Images for the front page should go in static/images. The frontpage
banner MUST be named course_image.jpg
\ No newline at end of file
**********************************************
Xml format of "Word Cloud" module [xmodule]
**********************************************
.. module:: word_cloud
Format description
==================
The main tag of Word Cloud module input is:
.. code-block:: xml
<word_cloud />
The following attributes can be specified for this tag::
[display_name| AUTOGENERATE] – Display name of xmodule. When this attribute is not defined - display name autogenerate with some hash.
[num_inputs| 5] – Number of inputs.
[num_top_words| 250] – Number of max words, which will be displayed.
[display_student_percents| True] – Display usage percents for each word.
.. note::
If you want to use the same word cloud (the same storage of words), you must use the same display_name value.
Code Example
============
Examples of word_cloud without all attributes (all attributes get by default)
-----------------------------------------------------------------------------
.. code-block:: xml
<word_cloud />
Examples of poll with all attributes
------------------------------------
.. code-block:: xml
<word_cloud display_name="cloud" num_inputs="10" num_top_words="100" />
Screenshots
===========
.. image:: word_cloud.png
:width: 50%
......@@ -26,6 +26,7 @@ Specific Problem Types
course_data_formats/graphical_slider_tool/graphical_slider_tool.rst
course_data_formats/poll_module/poll_module.rst
course_data_formats/conditional_module/conditional_module.rst
course_data_formats/word_cloud/word_cloud.rst
course_data_formats/custom_response.rst
......
......@@ -165,6 +165,13 @@ Video
:members:
:show-inheritance:
Word Cloud
==========
.. automodule:: xmodule.word_cloud_module
:members:
:show-inheritance:
X
=
......
......@@ -11,6 +11,7 @@ from util.cache import cache
import datetime
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
import datetime
log = logging.getLogger(__name__)
......@@ -104,6 +105,25 @@ def peer_grading_notifications(course, user):
def combined_notifications(course, user):
"""
Show notifications to a given user for a given course. Get notifications from the cache if possible,
or from the grading controller server if not.
@param course: The course object for which we are getting notifications
@param user: The user object for which we are getting notifications
@return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification
image), and response (actual response from grading controller server).
"""
#Set up return values so that we can return them for error cases
pending_grading = False
img_path = ""
notifications={}
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
#We don't want to show anonymous users anything.
if not user.is_authenticated():
return notification_dict
#Define a mock modulesystem
system = ModuleSystem(
ajax_url=None,
track_function=None,
......@@ -112,41 +132,44 @@ def combined_notifications(course, user):
replace_urls=None,
xblock_model_data= {}
)
#Initialize controller query service using our mock system
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
student_id = unique_id_for_user(user)
user_is_staff = has_access(user, course, 'staff')
course_id = course.id
notification_type = "combined"
#See if we have a stored value in the cache
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
if success:
return notification_dict
min_time_to_query = user.last_login
#Get the time of the last login of the user
last_login = user.last_login
#Find the modules they have seen since they logged in
last_module_seen = StudentModule.objects.filter(student=user, course_id=course_id,
modified__gt=min_time_to_query).values('modified').order_by(
modified__gt=last_login).values('modified').order_by(
'-modified')
last_module_seen_count = last_module_seen.count()
if last_module_seen_count > 0:
#The last time they viewed an updated notification (last module seen minus how long notifications are cached)
last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60))
else:
last_time_viewed = user.last_login
pending_grading = False
#If they have not seen any modules since they logged in, then don't refresh
return {'pending_grading': False, 'img_path': img_path, 'response': notifications}
img_path = ""
try:
#Get the notifications from the grading controller
controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff,
last_time_viewed)
log.debug(controller_response)
notifications = json.loads(controller_response)
if notifications['success']:
if notifications['overall_need_to_check']:
pending_grading = True
except:
#Non catastrophic error, so no real action
notifications = {}
#This is a dev_facing_error
log.exception(
"Problem with getting notifications from controller query service for course {0} user {1}.".format(
......@@ -157,6 +180,7 @@ def combined_notifications(course, user):
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
#Store the notifications in the cache
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
return notification_dict
......
......@@ -64,6 +64,7 @@ CACHES = ENV_TOKENS['CACHES']
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
......@@ -265,6 +265,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = (
('edX Admins', 'admin@edx.org'),
)
......
......@@ -37,6 +37,10 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
else:
response = requests.request(method, url, params=data_or_params, timeout=5)
except Exception as err:
# remove API key if it is in the params
if 'api_key' in data_or_params:
log.info('Deleting API key from params')
del data_or_params['api_key']
log.exception("Trying to call {method} on {url} with params {params}".format(
method=method, url=url, params=data_or_params))
# Reraise with a single exception type
......
......@@ -149,7 +149,7 @@
}
label {
color: #999;
color: #646464;
&.field-error {
display: block;
......
......@@ -156,7 +156,7 @@
<div id="calculator_wrapper">
<form id="calculator">
<div class="input-wrapper">
<input type="text" id="calculator_input" />
<input type="text" id="calculator_input" title="Calculator Input Field" />
<div class="help-wrapper">
<a href="#">Hints</a>
......@@ -176,8 +176,8 @@
</dl>
</div>
</div>
<input id="calculator_button" type="submit" value="="/>
<input type="text" id="calculator_output" readonly />
<input id="calculator_button" type="submit" title="Calculate" value="="/>
<input type="text" id="calculator_output" title="Calculator Output Field" readonly />
</form>
</div>
......
......@@ -12,19 +12,19 @@
</div>
<form id="pwd_reset_form" action="${reverse('password_reset')}" method="post" data-remote="true">
<label for="id_email">E-mail address:</label>
<input id="id_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/>
<label for="pwd_reset_email">E-mail address:</label>
<input id="pwd_reset_email" type="email" name="email" maxlength="75" placeholder="Your E-mail"/>
<div class="submit">
<input type="submit" id="pwd_reset_button" value="Reset my password" />
</div>
</form>
</div>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -40,5 +40,10 @@
$('#pwd_error').stop().css("display", "block");
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
<%namespace name='static' file='static_content.html'/>
<%! from datetime import datetime %>
<%! import pytz %>
<%! from django.conf import settings %>
<%! from courseware.tabs import get_discussion_link %>
......@@ -79,9 +81,16 @@ discussion_link = get_discussion_link(course) if course else None
<hr>
</header>
<%
dst = datetime.now(pytz.utc).astimezone(pytz.timezone("America/New_York")).dst()
business_hours = "13:00 UTC to 21:00 UTC" if dst else "14:00 UTC to 22:00 UTC"
%>
<p>
Thanks for your feedback. We will read your message, and our
support team may contact you to respond or ask for further clarification.
Thank you for your inquiry or feedback. We typically respond to a
request within one business day (Monday to Friday,
${business_hours}.) In the meantime, please review our
<a href="/help" target="_blank">detailed FAQs</a>
where most questions have already been answered.
</p>
<div class="close-modal">
......
......@@ -9,14 +9,17 @@
</header>
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
<label>E-mail</label>
<input name="email" type="email">
<label>Password</label>
<input name="password" type="password">
<label class="remember-me">
<input name="remember" type="checkbox" value="true">
<label for="login_email">E-mail</label>
<input id="login_email" type="email" name="email" placeholder="e.g. yourname@domain.com" />
<label for="login_password">Password</label>
<input id="login_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" />
<label for="login_remember_me" class="remember-me">
<input id="login_remember_me" type="checkbox" name="remember" value="true" />
Remember me
</label>
<div class="submit">
<input name="submit" type="submit" value="Access My Courses">
</div>
......@@ -34,11 +37,11 @@
% endif
</section>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -59,5 +62,10 @@
$('#login_error').html(json.value).stop().css("display", "block");
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
......@@ -10,7 +10,8 @@
<li>
<a class="seq_${item['type']} inactive progress-${item['progress_status']}"
data-id="${item['id']}"
data-element="${idx+1}">
data-element="${idx+1}"
href="javascript:void(0);">
<p>${item['title']}</p>
</a>
</li>
......
......@@ -20,27 +20,31 @@
<div class="input-group">
% if has_extauth_info is UNDEFINED:
<label data-field="email">E-mail*</label>
<input name="email" type="email" placeholder="eg. yourname@domain.com">
<label data-field="password">Password*</label>
<input name="password" type="password" placeholder="****">
<label data-field="username">Public Username*</label>
<input name="username" type="text" placeholder="Shown on forums">
<label data-field="name">Full Name*</label>
<input name="name" type="text" placeholder="For your certificate">
<label data-field="email" for="signup_email">E-mail *</label>
<input id="signup_email" type="email" name="email" placeholder="e.g. yourname@domain.com" required />
<label data-field="password" for="signup_password">Password *</label>
<input id="signup_password" type="password" name="password" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" required />
<label data-field="username" for="signup_username">Public Username *</label>
<input id="signup_username" type="text" name="username" placeholder="e.g. yourname (shown on forums)" required />
<label data-field="name" for="signup_fullname">Full Name *</label>
<input id="signup_fullname" type="text" name="name" placeholder="e.g. Your Name (for certificates)" required />
% else:
<p><i>Welcome</i> ${extauth_email}</p><br/>
<p><i>Enter a public username:</i></p>
<label data-field="username">Public Username*</label>
<input name="username" type="text" value="${extauth_username}" placeholder="Shown on forums">
<label data-field="username" for="signup_username">Public Username *</label>
<input id="signup_username" type="text" name="username" value="${extauth_username}" placeholder="e.g. yourname (shown on forums)" required />
% endif
</div>
<div class="input-group">
<section class="citizenship">
<label data-field="level_of_education">Ed. completed</label>
<label data-field="level_of_education" for="signup_ed_level">Ed. Completed</label>
<div class="input-wrapper">
<select name="level_of_education">
<select id="signup_ed_level" name="level_of_education">
<option value="">--</option>
%for code, ed_level in UserProfile.LEVEL_OF_EDUCATION_CHOICES:
<option value="${code}">${ed_level}</option>
......@@ -50,9 +54,9 @@
</section>
<section class="gender">
<label data-field="gender">Gender</label>
<label data-field="gender" for="signup_gender">Gender</label>
<div class="input-wrapper">
<select name="gender">
<select id="signup_gender" name="gender">
<option value="">--</option>
%for code, gender in UserProfile.GENDER_CHOICES:
<option value="${code}">${gender}</option>
......@@ -62,9 +66,9 @@
</section>
<section class="date-of-birth">
<label data-field="date-of-birth">Year of birth</label>
<label data-field="date-of-birth" for="signup_birth_year">Year of birth</label>
<div class="input-wrapper">
<select name="year_of_birth">
<select id="signup_birth_year" name="year_of_birth">
<option value="">--</option>
%for year in UserProfile.VALID_YEARS:
<option value="${year}">${year}</option>
......@@ -74,22 +78,23 @@
</div>
</section>
<label data-field="mailing_address">Mailing address</label>
<textarea name="mailing_address"></textarea>
<label data-field="goals">Goals in signing up for edX</label>
<textarea name="goals"></textarea>
<label data-field="mailing_address" for="signup_mailing_address">Mailing address</label>
<textarea id="signup_mailing_address" name="mailing_address"></textarea>
<label data-field="goals" for="signup_goals">Goals in signing up for edX</label>
<textarea name="goals" id="signup_goals"></textarea>
</div>
<div class="input-group">
<label data-field="terms_of_service" class="terms-of-service">
<input name="terms_of_service" type="checkbox" value="true">
<label data-field="terms_of_service" class="terms-of-service" for="signup_tos">
<input id="signup_tos" name="terms_of_service" type="checkbox" value="true">
I agree to the
<a href="${reverse('tos')}" target="_blank">Terms of Service</a>*
</label>
<label data-field="honor_code" class="honor-code">
<input name="honor_code" type="checkbox" value="true">
<label data-field="honor_code" class="honor-code" for="signup_honor">
<input id="signup_honor" name="honor_code" type="checkbox" value="true">
I agree to the
<a href="${reverse('honor')}" target="_blank">Honor Code</a>*
</label>
......@@ -110,11 +115,11 @@
</div>
<div class="close-modal">
<a href="#" class="close-modal" title="Close Modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</a>
</div>
</section>
......@@ -129,5 +134,10 @@
$("[data-field='"+json.field+"']").addClass('field-error')
}
});
// removing close link's default behavior
$('#login-modal .close-modal').click(function(e) {
e.preventDefault();
});
})(this)
</script>
......@@ -374,35 +374,6 @@
</div>
</article>
<article id="devops-engineer-systems-administrator" class="job">
<div class="inner-wrapper">
<h3><strong>DEVOPS ENGINEER – SYSTEMS ADMINISTRATOR</strong></h3>
<p>The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.</p>
<p>In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.</p>
<p><strong>Responsibilities:</strong></p>
<ul>
<li>Work with developers and staff to maintain and improve the infrastructure of edX.</li>
<li>Assist where needed with other technical support tasks to support the fast moving pace of edX.</li>
<li>Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.</li>
</ul>
<p><strong>Requirements:</strong></p>
<ul>
<li>Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
<li>Three or more years of systems administration. </li>
<li>Must have an excellent working knowledge of Linux both as an end-user and as an administrator.</li>
<li>Must be adept in programming/scripting languages such as Python, Ruby, Bash.</li>
<li>Must be familiar with a configuration management system such as Puppet, Chef, Ansible.</li>
<li>Must have experience running web applications in a production environment.</li>
<li>Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.</li>
<li>Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="learning-sciences-engineer" class="job">
<div class="inner-wrapper">
<h3><strong>LEARNING SCIENCES ENGINEER</strong></h3>
......@@ -483,39 +454,7 @@ development and program management teams.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="web-designer" class="job">
<div class="inner-wrapper">
<h3><strong>WEB DESIGNER, PRODUCT TEAM</strong></h3>
<p>EdX is looking for a Web Designer to join our Product Team and shape the experience of edX's online learning tools. With thousands and thousands of students and hundreds of professors using our software every day, our online learning tools have to sing. Our ideal candidates are passionate and picky about what makes a good user experience; sweat the mechanical, visual, and transactional details when designing; know how to bring an idea or project from a sketch on paper to being alive in a browser; can instinctually bring organization to a design meeting, deliverable, or project; and thrive on collaboration with colleagues and constant iteration/refinement.</p>
<p>As an edX Designer, you:</p>
<ul>
<li>Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.</li>
<li>Understand established interactive technologies and possess an undying thirst to learn about new ones.</li>
<li>Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.</li>
<li>Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.</li>
<li>Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.</li>
<li>Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.</li>
<li>Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.</li>
<li>Have a perfectionist mindset, but won’t lose momentum in projects because of it.</li>
<li>Expertly present user experience and design recommendations to team members.</li>
</ul>
<p><strong>Requirements:</strong></p>
<ul>
<li>Have at least 2 years of professional, post-collegiate experience.</li>
<li>Have a BA, BS, BFA, or equivalent work experience in areas such as human-computer interaction, information science, graphic or industrial design, computer science, fine arts, social sciences such as psychology, or another related field. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
</ul>
<p><strong>About the Product Design Team:</strong></p>
<p>We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life. We enjoy holding Design Studio exercises, finding the right design tool to do the job efficiently, and our CSS preprocessors.</p>
<p>If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Designer role at edX, and online samples of your work to <a href="mailto:jobs@edx.org">jobs@edx.org</a>. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.</p>
</div>
</article>
<article id="front-end-developer" class="job">
<article id="front-end-developer" class="job">
<div class="inner-wrapper">
<h3><strong>FRONT END DEVELOPER</strong></h3>
<p>edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.</p>
......@@ -545,6 +484,72 @@ development and program management teams.</p>
<p>If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to <a href="mailto:jobs@edx.org">jobs@edx.org</a>. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.</p>
</div>
</article>
<article id="test-engineer" class="job">
<div class="inner-wrapper">
<h3><strong>TEST ENGINEER</strong></h3>
<p>EdX is looking for a Software Engineer in Test to help architect and implement improvements to our testing infrastructure and write code to validate and verify development and deployment of our MOOC platform.</p>
<p>You are an experienced professional who is passionate about and current with cutting edge methodologies and practices for delivering high quality software. For example, you understand and can articulate the difference between BDD and TDD. You champion for developers to be confident in the quality of their code by giving them the tools they need to create and execute their own tests. You write unit tests that follow best practices for each layer of an MVC architecture. You work side by side with the DevOps team to define environments and automate their buildouts.</p>
<p><strong>Responsibilities:</strong></p>
<ul>
<li>Review software designs with a focus on code quality, risk, and testability</li>
<li>Build tools and frameworks that enable fellow engineers be more productive, write better code and test it themselves</li>
<li>Code test automation at all levels including class library, web application framework, javascript, and end-to-end</li>
<li>Enable metrics collection to measure adoption and expand the reach of the delivered tools</li>
<li>Fix framework bugs and improve test architecture, including adding required unit tests</li>
<li>Train and mentor other team members</li>
</ul>
<p><strong>Qualifications:</strong></p>
<ul>
<li>Excellent coding skills across a number of languages: Python or other high level programming languages, Javascript, bash, etc.</li>
<li>Experience in building test automation frameworks</li>
<li>Comfortable with source code in various languages (Python/Django, Ruby/Rails, Javascript/Backbone/JQuery, etc.)</li>
<li>Highly proficient in a Unix/Linux environment</li>
<li>Experience with database technologies from SQLite to MongoDB</li>
<li>Familiar with deployment automation (Puppet, Jenkins, AWS)</li>
<li>Open Source development experience preferred, extra points for sharing your GitHub / StackOverflow / etc. profile</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="coordinator-university-business-affairs" class="job">
<div class="inner-wrapper">
<h3><strong>COORDINATOR OF UNIVERSITY AND BUSINESS AFFAIRS</strong></h3>
<p>EdX is looking for a Coordinator of External Affairs, to streamline, organize and maintain our efforts in Business Development and University Relations.</p>
<p><strong>There are 4 primary areas of responsibility:</strong></p>
<ol>
<li>To ensure all visits to and from the edX offices by any partners and affiliates are managed, coordinated, and documented. This involves developing itineraries, booking flights and schedules, and managing meetings and events in concert with members of our executive team in University Relations and Business Development and our consortium of partners.</li>
<li>To maintain a database of partners and prospects and manage any data flows/reporting required.</li>
<li>To manage the information flow, recording activity on the edX Wiki page by synthesizing data and analysis from all visits and meetings and create updates on the edX Wiki page.</li>
<li>To act as a central point of contact for all relationship and event activity within this scope.</li>
</ol>
<p><strong>Detailed Responsibilities:</strong></p>
<ul>
<li>Provide support and coordinate activities for these 3 executives</li>
<li>Acquire strong user knowledge of related systems, processes and tools</li>
<li>Participate in the new partner on-boarding process</li>
<li>Provide an escalation point for Sales personnel for systems, procedures and policies</li>
<li>Maintain Salesforce database for client/partner set up and support information, generating reports as needed</li>
<li>Document proofreading, editing as directed for proposals, contracts, contact and call reports</li>
<li>Coordinate and manage travel, events and meetings, including invitations, RSVP’s, hotel/meeting space contracts, and providing event materials to attendees</li>
</ul>
<p><strong>Qualifications:</strong></p>
<ul>
<li>5-7 years of experience in a similar project/coordinator type position with progressively responsible administrative experience</li>
<li>Self-starter, possessing tenacity and a desire for challenges, not afraid to take risks, and the initiative to get things done with little direction </li>
<li>Superior interpersonal and communications skills, including concise writing and editing skills</li>
<li>Strong organizational skills to manage multiple competing priorities and projects with attention to detail </li>
<li>Exceptional ability to effectively interact with multiple external and internal stakeholders </li>
<li>Adept at analyzing complex issues with the ability to synthesize data and perform gap analyses</li>
<li>Performs well with a variety of disciplines while remaining effective in a high-volume, fast-pace start-up environment with high workload</li>
<li>Must be proficient in: MS PowerPoint, Word and Excel, Salesforce.com, and online tools such as Google docs and Wiki, and knowledge of Kanban is also helpful</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
</section>
......@@ -559,11 +564,11 @@ development and program management teams.</p>
<a href="#director-of-product-management">Director, Product Management</a>
<a href="#content-engineer">Content Engineer</a>
<a href="#software-engineer">Software Engineer</a>
<a href="#devops-engineer-systems-administrator">Devops Engineer - Systems Administrator</a>
<a href="#learning-sciences-engineer">Learning Sciences Engineer</a>
<a href="#sales-engineer">Sales Engineer, Business Development Team</a>
<a href="#web-designer">Web Designer, Product Team</a>
<a href="#front-end-developer">Front End Developer</a>
<a href="#test-engineer">Test Engineer</a>
<a href="#coordinator-university-business-affairs">Coordinator of University and Business Affairs</a>
</nav>
<h2>How to Apply</h2>
<p>E-mail your resume, cover letter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
......
<section
id="word_cloud_${element_id}"
class="${element_class}"
data-ajax-url="${ajax_url}"
>
<section class="input_cloud_section">
% for row in range(num_inputs):
<input
class="input-cloud"
${'style="display: none;"' if submitted else ''}
type="text"
size="40"
/>
% endfor
<section class="action">
<input class="save" type="button" value="Save" />
</section>
</section>
<section id="result_cloud_section_${element_id}" class="result_cloud_section">
<h3>Your words: <span class="your_words"></span></h3>
<h3>Total number of words: <span class="total_num_words"></span></h3>
<div class="word_cloud"></div>
</section>
</section>
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