'
+ );
+
+ draggableObj.labelEl.appendTo(draggableObj.containerEl);
+ draggableObj.labelWidth = draggableObj.labelEl.width();
+ draggableObj.labelEl.css({
+ 'left': 50 - draggableObj.labelWidth * 0.5,
+ 'top': 5 + draggableObj.iconHeightSmall + 5
+ });
+
+ draggableObj.attachMouseEventsTo('labelEl');
+ }
+
+ draggableObj.hasLoaded = true;
+ });
+ } else {
+ // To make life easier, if there is no icon, but there is a
+ // label, we will create a label and store it as if it was an
+ // icon. All the existing code will work, and the user will
+ // see a label instead of an icon.
+ if (obj.label.length > 0) {
+ draggableObj.iconElBGColor = state.config.labelBgColor;
+ draggableObj.iconElPadding = 8;
+ draggableObj.iconElBorder = '1px solid black';
+ draggableObj.iconElLeftOffset = 9;
+
+ draggableObj.iconEl = $(
+ '
' +
+ obj.label +
+ '
'
+ );
+
+ draggableObj.iconEl.appendTo(draggableObj.containerEl);
+
+ draggableObj.iconWidth = draggableObj.iconEl.width();
+ draggableObj.iconHeight = draggableObj.iconEl.height();
+ draggableObj.iconWidthSmall = draggableObj.iconWidth;
+ draggableObj.iconHeightSmall = draggableObj.iconHeight;
+
+ draggableObj.iconEl.css({
+ 'left': 50 - draggableObj.iconWidthSmall * 0.5,
+ 'top': 50 - draggableObj.iconHeightSmall * 0.5
+ });
+
+ draggableObj.hasLoaded = true;
+ } else {
+ // If no icon and no label, don't create a draggable.
+ return;
+ }
+ }
+
+ draggableObj.attachMouseEventsTo('iconEl');
+ draggableObj.attachMouseEventsTo('containerEl');
+
+ state.numDraggablesInSlider += 1;
+ draggableObj.stateDraggablesIndex = state.draggables.push(draggableObj) - 1;
+ }
+
+ function mouseDown(event) {
+ if (this.mousePressed === false) {
+ // So that the browser does not perform a default drag.
+ // If we don't do this, each drag operation will
+ // potentially cause the highlghting of the dragged element.
+ event.preventDefault();
+ event.stopPropagation();
+
+ // If this draggable is just being dragged out of the
+ // container, we must perform some additional tasks.
+ if (this.inContainer === true) {
+ if ((this.isReusable === true) && (this.isOriginal === true)) {
+ this.makeDraggableCopy(function (draggableCopy) {
+ draggableCopy.mouseDown(event);
+ });
+
+ return;
+ }
+
+ if (this.isOriginal === true) {
+ this.containerEl.hide();
+ this.iconEl.detach();
+ }
+ this.iconEl.css({
+ 'background-color': this.iconElBGColor,
+ 'padding-left': this.iconElPadding,
+ 'padding-right': this.iconElPadding,
+ 'border': this.iconElBorder,
+ 'width': this.iconWidth,
+ 'height': this.iconHeight,
+ 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
+ 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
+ });
+ this.iconEl.appendTo(this.state.baseImageEl.parent());
+
+ if (this.labelEl !== null) {
+ if (this.isOriginal === true) {
+ this.labelEl.detach();
+ }
+ this.labelEl.css({
+ 'background-color': this.state.config.labelBgColor,
+ 'padding-left': 8,
+ 'padding-right': 8,
+ 'border': '1px solid black',
+ 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Account for padding, border.
+ 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
+ });
+ this.labelEl.appendTo(this.state.baseImageEl.parent());
+ }
+
+ this.inContainer = false;
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider -= 1;
+ }
+ }
+
+ this.zIndex = 1000;
+ this.iconEl.css('z-index', '1000');
+ if (this.labelEl !== null) {
+ this.labelEl.css('z-index', '1000');
+ }
+
+ this.mousePressed = true;
+ this.state.currentMovingDraggable = this;
+ }
+ }
+
+ function mouseUp() {
+ if (this.mousePressed === true) {
+ this.state.currentMovingDraggable = null;
+
+ this.checkLandingElement();
+ }
+ }
+
+ function mouseMove(event) {
+ if (this.mousePressed === true) {
+ // Because we have also attached a 'mousemove' event to the
+ // 'document' (that will do the same thing), let's tell the
+ // browser not to bubble up this event. The attached event
+ // on the 'document' will only be triggered when the mouse
+ // pointer leaves the draggable while it is in the middle
+ // of a drag operation (user moves the mouse very quickly).
+ event.stopPropagation();
+
+ this.iconEl.css({
+ 'left': event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
+ 'top': event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
+ });
+
+ if (this.labelEl !== null) {
+ this.labelEl.css({
+ 'left': event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Acoount for padding, border.
+ 'top': event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
+ });
+ }
+ }
+ }
+
+ // At this point the mouse was realeased, and we need to check
+ // where the draggable eneded up. Based on several things, we
+ // will either move the draggable back to the slider, or update
+ // the input with the user's answer (X-Y position of the draggable,
+ // or the ID of the target where it landed.
+ function checkLandingElement() {
+ var positionIE;
+
+ this.mousePressed = false;
+ positionIE = this.iconEl.position();
+
+ if (this.state.config.individualTargets === true) {
+ if (this.checkIfOnTarget(positionIE) === true) {
+ this.correctZIndexes();
+ } else {
+ if (this.onTarget !== null) {
+ this.onTarget.removeDraggable(this);
+ }
+
+ this.moveBackToSlider();
+
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider += 1;
+ }
+ }
+ } else {
+ if (
+ (positionIE.left < 0) ||
+ (positionIE.left + this.iconWidth > this.state.baseImageEl.width()) ||
+ (positionIE.top < 0) ||
+ (positionIE.top + this.iconHeight > this.state.baseImageEl.height())
+ ) {
+ this.moveBackToSlider();
+
+ this.x = -1;
+ this.y = -1;
+
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider += 1;
+ }
+ } else {
+ this.correctZIndexes();
+
+ this.x = positionIE.left + this.iconWidth * 0.5;
+ this.y = positionIE.top + this.iconHeight * 0.5;
+ }
+ }
+
+ if (this.isOriginal === true) {
+ this.state.updateArrowOpacity();
+ }
+ updateInput.update(this.state);
+ }
+
+ // Determine if a draggable, after it was relased, ends up on a
+ // target. We do this by iterating over all of the targets, and
+ // for each one we check whether the draggable's center is
+ // within the target's dimensions.
+ //
+ // positionIE is the object as returned by
+ //
+ // this.iconEl.position()
+ function checkIfOnTarget(positionIE) {
+ var c1, target;
+
+ for (c1 = 0; c1 < this.state.targets.length; c1 += 1) {
+ target = this.state.targets[c1];
+
+ // If only one draggable per target is allowed, and
+ // the current target already has a draggable on it
+ // (with an ID different from the one we are checking
+ // against), then go to next target.
+ if (
+ (this.state.config.onePerTarget === true) &&
+ (target.draggableList.length === 1) &&
+ (target.draggableList[0].uniqueId !== this.uniqueId)
+ ) {
+ continue;
+ }
+
+ // Check if the draggable's center coordinate is within
+ // the target's dimensions. If not, go to next target.
+ if (
+ (positionIE.top + this.iconHeight * 0.5 < target.offset.top) ||
+ (positionIE.top + this.iconHeight * 0.5 > target.offset.top + target.h) ||
+ (positionIE.left + this.iconWidth * 0.5 < target.offset.left) ||
+ (positionIE.left + this.iconWidth * 0.5 > target.offset.left + target.w)
+ ) {
+ continue;
+ }
+
+ // If the draggable was moved from one target to
+ // another, then we need to remove it from the
+ // previous target's draggables list, and add it to the
+ // new target's draggables list.
+ if ((this.onTarget !== null) && (this.onTarget.id !== target.id)) {
+ this.onTarget.removeDraggable(this);
+ target.addDraggable(this);
+ }
+ // If the draggable was moved from the slider to a
+ // target, remember the target, and add ID to the
+ // target's draggables list.
+ else if (this.onTarget === null) {
+ target.addDraggable(this);
+ }
+
+ // Reposition the draggable so that it's center
+ // coincides with the center of the target.
+ this.snapToTarget(target);
+
+ // Target was found.
+ return true;
+ }
+
+ // Target was not found.
+ return false;
+ }
+
+ function snapToTarget(target) {
+ var offset;
+
+ offset = 0;
+ if (this.state.config.targetOutline === true) {
+ offset = 1;
+ }
+
+ this.iconEl.css({
+ 'left': target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
+ 'top': target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset
+ });
+
+ if (this.labelEl !== null) {
+ this.labelEl.css({
+ 'left': target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Acoount for padding, border.
+ 'top': target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset
+ });
+ }
+ }
+
+ // Go through all of the draggables subtract 1 from the z-index
+ // of all whose z-index is higher than the old z-index of the
+ // current element. After, set the z-index of the current
+ // element to 1 + N (where N is the number of draggables - i.e.
+ // the highest z-index possible).
+ //
+ // This will make sure that after releasing a draggable, it
+ // will be on top of all of the other draggables. Also, the
+ // ordering of the visibility (z-index) of the other draggables
+ // will not change.
+ function correctZIndexes() {
+ var c1, highestZIndex;
+
+ highestZIndex = -10000;
+
+ if (this.state.config.individualTargets === true) {
+ if (this.onTarget.draggableList.length > 0) {
+ for (c1 = 0; c1 < this.onTarget.draggableList.length; c1 += 1) {
+ if (
+ (this.onTarget.draggableList[c1].zIndex > highestZIndex) &&
+ (this.onTarget.draggableList[c1].zIndex !== 1000)
+ ) {
+ highestZIndex = this.onTarget.draggableList[c1].zIndex;
+ }
+ }
+ } else {
+ highestZIndex = 0;
+ }
+ } else {
+ for (c1 = 0; c1 < this.state.draggables.length; c1++) {
+ if (this.inContainer === false) {
+ if (
+ (this.state.draggables[c1].zIndex > highestZIndex) &&
+ (this.state.draggables[c1].zIndex !== 1000)
+ ) {
+ highestZIndex = this.state.draggables[c1].zIndex;
+ }
+ }
+ }
+ }
+
+ if (highestZIndex === -10000) {
+ highestZIndex = 0;
+ }
+
+ this.zIndex = highestZIndex + 1;
+
+ this.iconEl.css('z-index', this.zIndex);
+ if (this.labelEl !== null) {
+ this.labelEl.css('z-index', this.zIndex);
+ }
+ }
+
+ // If a draggable was released in a wrong positione, we will
+ // move it back to the slider, placing it in the same position
+ // that it was dragged out of.
+ function moveBackToSlider() {
+ var c1;
+
+ if (this.isOriginal === false) {
+ this.iconEl.remove();
+ if (this.labelEl !== null) {
+ this.labelEl.remove();
+ }
+ this.state.draggables.splice(this.stateDraggablesIndex, 1);
+
+ for (c1 = 0; c1 < this.state.draggables; c1 += 1) {
+ if (this.state.draggables[c1].stateDraggablesIndex > this.stateDraggablesIndex) {
+ this.state.draggables[c1].stateDraggablesIndex -= 1;
+ }
+ }
+
+ return;
+ }
+
+ this.containerEl.show();
+ this.zIndex = 1;
+
+ this.iconEl.detach();
+ this.iconEl.css({
+ 'border': 'none',
+ 'background-color': 'transparent',
+ 'padding-left': 0,
+ 'padding-right': 0,
+ 'z-index': this.zIndex,
+ 'width': this.iconWidthSmall,
+ 'height': this.iconHeightSmall,
+ 'left': 50 - this.iconWidthSmall * 0.5,
+ 'top': ((this.labelEl !== null) ? 5 : 50 - this.iconHeightSmall * 0.5)
+ });
+ this.iconEl.appendTo(this.containerEl);
+
+ if (this.labelEl !== null) {
+ this.labelEl.detach();
+ this.labelEl.css({
+ 'border': 'none',
+ 'background-color': 'transparent',
+ 'padding-left': 0,
+ 'padding-right': 0,
+ 'z-index': this.zIndex,
+ 'left': 50 - this.labelWidth * 0.5,
+ 'top': 5 + this.iconHeightSmall + 5
+ });
+ this.labelEl.appendTo(this.containerEl);
+ }
+
+ this.inContainer = true;
+ }
+});
+
+// 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)
diff --git a/common/static/js/capa/drag_and_drop/logme.js b/common/static/js/capa/drag_and_drop/logme.js
new file mode 100644
index 0000000000..21f73bf2a5
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/logme.js
@@ -0,0 +1,36 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define([], function () {
+ var debugMode;
+
+ debugMode = true;
+
+ return logme;
+
+ function logme() {
+ var i;
+
+ if (
+ (debugMode !== true) ||
+ (typeof window.console === 'undefined')
+ ) {
+ return;
+ }
+
+ i = 0;
+ while (i < arguments.length) {
+ window.console.log(arguments[i]);
+ i += 1;
+ }
+ }
+});
+
+// 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)
diff --git a/common/static/js/capa/drag_and_drop/main.js b/common/static/js/capa/drag_and_drop/main.js
new file mode 100644
index 0000000000..89cf08001d
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/main.js
@@ -0,0 +1,81 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define(
+ ['logme', 'state', 'config_parser', 'container', 'base_image', 'scroller', 'draggables', 'targets', 'update_input'],
+ function (logme, State, configParser, Container, BaseImage, Scroller, Draggables, Targets, updateInput) {
+ return Main;
+
+ function Main() {
+ $('.drag_and_drop_problem_div').each(processProblem);
+ }
+
+ // $(value) - get the element of the entire problem
+ function processProblem(index, value) {
+ var problemId, config, state;
+
+ if ($(value).attr('data-problem-processed') === 'true') {
+ // This problem was already processed by us before, so we will
+ // skip it.
+
+ return;
+ }
+ $(value).attr('data-problem-processed', 'true');
+
+ problemId = $(value).attr('data-plain-id');
+ if (typeof problemId !== 'string') {
+ logme('ERROR: Could not find the ID of the problem DOM element.');
+
+ return;
+ }
+
+ try {
+ config = JSON.parse($('#drag_and_drop_json_' + problemId).html());
+ } catch (err) {
+ logme('ERROR: Could not parse the JSON configuration options.');
+ logme('Error message: "' + err.message + '".');
+
+ return;
+ }
+
+ state = State(problemId);
+
+ if (configParser(state, config) !== true) {
+ logme('ERROR: Could not make sense of the JSON configuration options.');
+
+ return;
+ }
+
+ Container(state);
+ BaseImage(state);
+
+ (function addContent() {
+ if (state.baseImageLoaded !== true) {
+ setTimeout(addContent, 50);
+
+ return;
+ }
+
+ Targets(state);
+ Scroller(state);
+ Draggables.init(state);
+
+ state.updateArrowOpacity();
+
+ // Update the input element, checking first that it is not filled with
+ // an answer from the server.
+ if (updateInput.check(state) === false) {
+ updateInput.update(state);
+ }
+ }());
+ }
+});
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/common/static/js/capa/drag_and_drop/scroller.js b/common/static/js/capa/drag_and_drop/scroller.js
new file mode 100644
index 0000000000..c1fe867006
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/scroller.js
@@ -0,0 +1,215 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define(['logme'], function (logme) {
+ return Scroller;
+
+ function Scroller(state) {
+ var parentEl, moveLeftEl, showEl, moveRightEl, showElLeftMargin;
+
+ parentEl = $(
+ ''
+ );
+
+ moveLeftEl = $(
+ '
' +
+ '' +
+ '
'
+ );
+ moveLeftEl.appendTo(parentEl);
+
+ // The below is necessary to prevent the browser thinking that we want
+ // to perform a drag operation, or a highlight operation. If we don't
+ // do this, the browser will then highlight with a gray shade the
+ // element.
+ moveLeftEl.mousemove(function (event) { event.preventDefault(); });
+ moveLeftEl.mousedown(function (event) { event.preventDefault(); });
+
+ // This event will be responsible for moving the scroller left.
+ // Hidden draggables will be shown.
+ moveLeftEl.mouseup(function (event) {
+ event.preventDefault();
+
+ // When there are no more hidden draggables, prevent from
+ // scrolling infinitely.
+ if (showElLeftMargin > -102) {
+ return;
+ }
+
+ showElLeftMargin += 102;
+
+ // We scroll by changing the 'margin-left' CSS property smoothly.
+ state.sliderEl.animate({
+ 'margin-left': showElLeftMargin + 'px'
+ }, 100, function () {
+ updateArrowOpacity();
+ });
+ });
+
+ showEl = $(
+ ''
+ );
+ showEl.appendTo(parentEl);
+
+ showElLeftMargin = 0;
+
+ // Element where the draggables will be contained. It is very long
+ // so that any SANE number of draggables will fit in a single row. It
+ // will be contained in a parent element whose 'overflow' CSS value
+ // will be hidden, preventing the long row from fully being visible.
+ state.sliderEl = $(
+ ''
+ );
+ state.sliderEl.appendTo(showEl);
+
+ state.sliderEl.mousedown(function (event) {
+ event.preventDefault();
+ });
+
+ moveRightEl = $(
+ '
' +
+ '' +
+ '
'
+ );
+ moveRightEl.appendTo(parentEl);
+
+ // The below is necessary to prevent the browser thinking that we want
+ // to perform a drag operation, or a highlight operation. If we don't
+ // do this, the browser will then highlight with a gray shade the
+ // element.
+ moveRightEl.mousemove(function (event) { event.preventDefault(); });
+ moveRightEl.mousedown(function (event) { event.preventDefault(); });
+
+ // This event will be responsible for moving the scroller right.
+ // Hidden draggables will be shown.
+ moveRightEl.mouseup(function (event) {
+ event.preventDefault();
+
+ // When there are no more hidden draggables, prevent from
+ // scrolling infinitely.
+ if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
+ return;
+ }
+
+ showElLeftMargin -= 102;
+
+ // We scroll by changing the 'margin-left' CSS property smoothly.
+ state.sliderEl.animate({
+ 'margin-left': showElLeftMargin + 'px'
+ }, 100, function () {
+ updateArrowOpacity();
+ });
+ });
+
+ parentEl.appendTo(state.containerEl);
+
+ // Make the function available throughout the application. We need to
+ // call it in several places:
+ //
+ // 1.) When initially reading answer from server, if draggables will be
+ // positioned on the base image, the scroller's right and left arrows
+ // opacity must be updated.
+ //
+ // 2.) When creating draggable elements, the scroller's right and left
+ // arrows opacity must be updated according to the number of
+ // draggables.
+ state.updateArrowOpacity = updateArrowOpacity;
+
+ return;
+
+ function updateArrowOpacity() {
+ moveLeftEl.children('div').css('opacity', '1');
+ moveRightEl.children('div').css('opacity', '1');
+
+ if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
+ moveRightEl.children('div').css('opacity', '.4');
+ }
+ if (showElLeftMargin > -102) {
+ moveLeftEl.children('div').css('opacity', '.4');
+ }
+ }
+ } // End-of: function Scroller(state)
+});
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/common/static/js/capa/drag_and_drop/state.js b/common/static/js/capa/drag_and_drop/state.js
new file mode 100644
index 0000000000..4565acd842
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/state.js
@@ -0,0 +1,105 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define([], function () {
+ return State;
+
+ function State(problemId) {
+ var state;
+
+ state = {
+ 'config': null,
+
+ 'baseImageEl': null,
+ 'baseImageLoaded': false,
+
+ 'containerEl': null,
+
+ 'sliderEl': null,
+
+ 'problemId': problemId,
+
+ 'draggables': [],
+ 'numDraggablesInSlider': 0,
+ 'currentMovingDraggable': null,
+
+ 'targets': [],
+
+ 'updateArrowOpacity': null,
+
+ 'uniqueId': 0,
+ 'salt': makeSalt(),
+
+ 'getUniqueId': getUniqueId
+ };
+
+ $(document).mousemove(function (event) {
+ documentMouseMove(state, event);
+ });
+
+ return state;
+ }
+
+ function getUniqueId() {
+ this.uniqueId += 1;
+
+ return this.salt + '_' + this.uniqueId.toFixed(0);
+ }
+
+ function makeSalt() {
+ var text, possible, i;
+
+ text = '';
+ possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ for(i = 0; i < 5; i += 1) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+
+ return text;
+ }
+
+ function documentMouseMove(state, event) {
+ if (state.currentMovingDraggable !== null) {
+ state.currentMovingDraggable.iconEl.css(
+ 'left',
+ event.pageX -
+ state.baseImageEl.offset().left -
+ state.currentMovingDraggable.iconWidth * 0.5
+ - state.currentMovingDraggable.iconElLeftOffset
+ );
+ state.currentMovingDraggable.iconEl.css(
+ 'top',
+ event.pageY -
+ state.baseImageEl.offset().top -
+ state.currentMovingDraggable.iconHeight * 0.5
+ );
+
+ if (state.currentMovingDraggable.labelEl !== null) {
+ state.currentMovingDraggable.labelEl.css(
+ 'left',
+ event.pageX -
+ state.baseImageEl.offset().left -
+ state.currentMovingDraggable.labelWidth * 0.5
+ - 9 // Account for padding, border.
+ );
+ state.currentMovingDraggable.labelEl.css(
+ 'top',
+ event.pageY -
+ state.baseImageEl.offset().top +
+ state.currentMovingDraggable.iconHeight * 0.5 +
+ 5
+ );
+ }
+ }
+ }
+});
+
+// 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)
diff --git a/common/static/js/capa/drag_and_drop/targets.js b/common/static/js/capa/drag_and_drop/targets.js
new file mode 100644
index 0000000000..e56020aac6
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/targets.js
@@ -0,0 +1,192 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define(['logme'], function (logme) {
+ return Targets;
+
+ function Targets(state) {
+ (function (c1) {
+ while (c1 < state.config.targets.length) {
+ processTarget(state, state.config.targets[c1]);
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ function processTarget(state, obj) {
+ var targetEl, borderCss, numTextEl, targetObj;
+
+ borderCss = '';
+ if (state.config.targetOutline === true) {
+ borderCss = 'border: 1px dashed gray; ';
+ }
+
+ targetEl = $(
+ ''
+ );
+ targetEl.appendTo(state.baseImageEl.parent());
+ targetEl.mousedown(function (event) {
+ event.preventDefault();
+ });
+
+ if (state.config.onePerTarget === false) {
+ numTextEl = $(
+ '
0
'
+ );
+ } else {
+ numTextEl = null;
+ }
+
+ targetObj = {
+ 'id': obj.id,
+
+ 'w': obj.w,
+ 'h': obj.h,
+
+ 'el': targetEl,
+ 'offset': targetEl.position(),
+
+ 'draggableList': [],
+
+ 'state': state,
+
+ 'targetEl': targetEl,
+
+ 'numTextEl': numTextEl,
+ 'updateNumTextEl': updateNumTextEl,
+
+ 'removeDraggable': removeDraggable,
+ 'addDraggable': addDraggable
+ };
+
+ if (state.config.onePerTarget === false) {
+ numTextEl.appendTo(state.baseImageEl.parent());
+ numTextEl.mousedown(function (event) {
+ event.preventDefault();
+ });
+ numTextEl.mouseup(function () {
+ cycleDraggableOrder.call(targetObj)
+ });
+ }
+
+ state.targets.push(targetObj);
+ }
+
+ function removeDraggable(draggable) {
+ var c1;
+
+ this.draggableList.splice(draggable.onTargetIndex, 1);
+
+ // An item from the array was removed. We need to updated all indexes accordingly.
+ // Shift all indexes down by one if they are higher than the index of the removed item.
+ c1 = 0;
+ while (c1 < this.draggableList.length) {
+ if (this.draggableList[c1].onTargetIndex > draggable.onTargetIndex) {
+ this.draggableList[c1].onTargetIndex -= 1;
+ }
+
+ c1 += 1;
+ }
+
+ draggable.onTarget = null;
+ draggable.onTargetIndex = null;
+
+ this.updateNumTextEl();
+ }
+
+ function addDraggable(draggable) {
+ draggable.onTarget = this;
+ draggable.onTargetIndex = this.draggableList.push(draggable) - 1;
+
+ this.updateNumTextEl();
+ }
+
+ /*
+ * function cycleDraggableOrder
+ *
+ * Parameters:
+ * none - This function does not expect any parameters.
+ *
+ * Returns:
+ * undefined - The return value of this function is not used.
+ *
+ * Description:
+ * Go through all draggables that are on the current target, and decrease their
+ * z-index by 1, making sure that the bottom-most draggable ends up on the top.
+ */
+ function cycleDraggableOrder() {
+ var c1, lowestZIndex, highestZIndex;
+
+ if (this.draggableList.length < 2) {
+ return;
+ }
+
+ highestZIndex = -10000;
+ lowestZIndex = 10000;
+
+ for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
+ if (this.draggableList[c1].zIndex < lowestZIndex) {
+ lowestZIndex = this.draggableList[c1].zIndex;
+ }
+
+ if (this.draggableList[c1].zIndex > highestZIndex) {
+ highestZIndex = this.draggableList[c1].zIndex;
+ }
+ }
+
+ for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
+ if (this.draggableList[c1].zIndex === lowestZIndex) {
+ this.draggableList[c1].zIndex = highestZIndex;
+ } else {
+ this.draggableList[c1].zIndex -= 1;
+ }
+
+ this.draggableList[c1].iconEl.css('z-index', this.draggableList[c1].zIndex);
+ if (this.draggableList[c1].labelEl !== null) {
+ this.draggableList[c1].labelEl.css('z-index', this.draggableList[c1].zIndex);
+ }
+ }
+ }
+
+ function updateNumTextEl() {
+ if (this.numTextEl !== null) {
+ this.numTextEl.html(this.draggableList.length);
+ }
+ }
+});
+
+// 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)
diff --git a/common/static/js/capa/drag_and_drop/update_input.js b/common/static/js/capa/drag_and_drop/update_input.js
new file mode 100644
index 0000000000..04715a3ecf
--- /dev/null
+++ b/common/static/js/capa/drag_and_drop/update_input.js
@@ -0,0 +1,227 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
+(function (requirejs, require, define) {
+
+define(['logme'], function (logme) {
+ return {
+ 'check': check,
+ 'update': update
+ };
+
+ function update(state) {
+ var draggables, tempObj;
+
+ draggables = [];
+
+ if (state.config.individualTargets === false) {
+ (function (c1) {
+ while (c1 < state.draggables.length) {
+ if (state.draggables[c1].x !== -1) {
+ tempObj = {};
+ tempObj[state.draggables[c1].id] = [
+ state.draggables[c1].x,
+ state.draggables[c1].y
+ ];
+ draggables.push(tempObj);
+ tempObj = null;
+ }
+
+ c1 += 1;
+ }
+ }(0));
+ } else {
+ (function (c1) {
+ while (c1 < state.targets.length) {
+ (function (c2) {
+ while (c2 < state.targets[c1].draggableList.length) {
+ tempObj = {};
+ tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id;
+ draggables.push(tempObj);
+ tempObj = null;
+
+ c2 += 1;
+ }
+ }(0));
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ $('#input_' + state.problemId).val(JSON.stringify({'draggables': draggables}));
+ }
+
+ // Check if input has an answer from server. If yes, then position
+ // all draggables according to answer.
+ function check(state) {
+ var inputElVal;
+
+ inputElVal = $('#input_' + state.problemId).val();
+ if (inputElVal.length === 0) {
+ return false;
+ }
+
+ repositionDraggables(state, JSON.parse(inputElVal));
+
+ return true;
+ }
+
+ function getUseTargets(answer) {
+ if ($.isArray(answer.draggables) === false) {
+ logme('ERROR: answer.draggables is not an array.');
+
+ return;
+ } else if (answer.draggables.length === 0) {
+ return;
+ }
+
+ if ($.isPlainObject(answer.draggables[0]) === false) {
+ logme('ERROR: answer.draggables array does not contain objects.');
+
+ return;
+ }
+
+ for (c1 in answer.draggables[0]) {
+ if (answer.draggables[0].hasOwnProperty(c1) === false) {
+ continue;
+ }
+
+ if (typeof answer.draggables[0][c1] === 'string') {
+ // use_targets = true;
+
+ return true;
+ } else if (
+ ($.isArray(answer.draggables[0][c1]) === true) &&
+ (answer.draggables[0][c1].length === 2)
+ ) {
+ // use_targets = false;
+
+ return false;
+ } else {
+ logme('ERROR: answer.draggables[0] is inconsidtent.');
+
+ return;
+ }
+ }
+
+ logme('ERROR: answer.draggables[0] is an empty object.');
+
+ return;
+ }
+
+ function processAnswerTargets(state, answer) {
+ var draggableId, draggable, targetId, target;
+
+ (function (c1) {
+ while (c1 < answer.draggables.length) {
+ for (draggableId in answer.draggables[c1]) {
+ if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
+ continue;
+ }
+
+ if ((draggable = getById(state, 'draggables', draggableId)) === null) {
+ logme(
+ 'ERROR: In answer there exists a ' +
+ 'draggable ID "' + draggableId + '". No ' +
+ 'draggable with this ID could be found.'
+ );
+
+ continue;
+ }
+
+ targetId = answer.draggables[c1][draggableId];
+ if ((target = getById(state, 'targets', targetId)) === null) {
+ logme(
+ 'ERROR: In answer there exists a target ' +
+ 'ID "' + targetId + '". No target with this ' +
+ 'ID could be found.'
+ );
+
+ continue;
+ }
+
+ draggable.moveDraggableTo('target', target);
+ }
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ function processAnswerPositions(state, answer) {
+ var draggableId, draggable;
+
+ (function (c1) {
+ while (c1 < answer.draggables.length) {
+ for (draggableId in answer.draggables[c1]) {
+ if (answer.draggables[c1].hasOwnProperty(draggableId) === false) {
+ continue;
+ }
+
+ if ((draggable = getById(state, 'draggables', draggableId)) === null) {
+ logme(
+ 'ERROR: In answer there exists a ' +
+ 'draggable ID "' + draggableId + '". No ' +
+ 'draggable with this ID could be found.'
+ );
+
+ continue;
+ }
+
+ draggable.moveDraggableTo('XY', {
+ 'x': answer.draggables[c1][draggableId][0],
+ 'y': answer.draggables[c1][draggableId][1]
+ });
+ }
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ function repositionDraggables(state, answer) {
+ if (answer.draggables.length === 0) {
+ return;
+ }
+
+ if (state.config.individualTargets !== getUseTargets(answer)) {
+ logme('ERROR: JSON config is not consistent with server response.');
+
+ return;
+ }
+
+ if (state.config.individualTargets === true) {
+ processAnswerTargets(state, answer);
+ } else if (state.config.individualTargets === false) {
+ processAnswerPositions(state, answer);
+ }
+ }
+
+ function getById(state, type, id) {
+ return (function (c1) {
+ while (c1 < state[type].length) {
+ if (type === 'draggables') {
+ if ((state[type][c1].id === id) && (state[type][c1].isOriginal === true)) {
+ return state[type][c1];
+ }
+ } else { // 'targets'
+ if (state[type][c1].id === id) {
+ return state[type][c1];
+ }
+ }
+
+ c1 += 1;
+ }
+
+ return null;
+ }(0));
+ }
+});
+
+// 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)
diff --git a/docs/source/drag-n-drop-demo.xml b/docs/source/drag-n-drop-demo.xml
new file mode 100644
index 0000000000..67712407a1
--- /dev/null
+++ b/docs/source/drag-n-drop-demo.xml
@@ -0,0 +1,526 @@
+
+
+
+
+
[Anyof rule example]
+
Please label hydrogen atoms connected with left carbon atom.
[Exact number of draggables for a set of targets.]
+
Drag two Grass and one Star to first or second positions, and three Cloud to any of the three positions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
[As many as you like draggables for a set of targets.]
+
Drag some Grass to any of the targets, and some Stars to either first or last target.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/source/drag_and_drop_input.rst b/docs/source/drag_and_drop_input.rst
new file mode 100644
index 0000000000..06a28a5926
--- /dev/null
+++ b/docs/source/drag_and_drop_input.rst
@@ -0,0 +1,323 @@
+**********************************************
+Xml format of drag and drop input [inputtypes]
+**********************************************
+
+.. module:: drag_and_drop_input
+
+Format description
+==================
+
+The main tag of Drag and Drop (DnD) input is::
+
+ ...
+
+``drag_and_drop_input`` can include any number of the following 2 tags:
+``draggable`` and ``target``.
+
+drag_and_drop_input tag
+-----------------------
+
+The main container for a single instance of DnD. The following attributes can
+be specified for this tag::
+
+ img - Relative path to an image that will be the base image. All draggables
+ can be dragged onto it.
+ target_outline - Specify whether an outline (gray dashed line) should be
+ drawn around targets (if they are specified). It can be either
+ 'true' or 'false'. If not specified, the default value is
+ 'false'.
+ one_per_target - Specify whether to allow more than one draggable to be
+ placed onto a single target. It can be either 'true' or 'false'. If
+ not specified, the default value is 'true'.
+ no_labels - default is false, in default behaviour if label is not set, label
+ is obtained from id. If no_labels is true, labels are not automatically
+ populated from id, and one can not set labels and obtain only icons.
+
+draggable tag
+-------------
+
+Draggable tag specifies a single draggable object which has the following
+attributes::
+
+ id - Unique identifier of the draggable object.
+ label - Human readable label that will be shown to the user.
+ icon - Relative path to an image that will be shown to the user.
+ can_reuse - true or false, default is false. If true, same draggable can be
+ used multiple times.
+
+A draggable is what the user must drag out of the slider and place onto the
+base image. After a drag operation, if the center of the draggable ends up
+outside the rectangular dimensions of the image, it will be returned back
+to the slider.
+
+In order for the grader to work, it is essential that a unique ID
+is provided. Otherwise, there will be no way to tell which draggable is at what
+coordinate, or over what target. Label and icon attributes are optional. If
+they are provided they will be used, otherwise, you can have an empty
+draggable. The path is relative to 'course_folder' folder, for example,
+/static/images/img1.png.
+
+target tag
+----------
+
+Target tag specifies a single target object which has the following required
+attributes::
+
+ id - Unique identifier of the target object.
+ x - X-coordinate on the base image where the top left corner of the target
+ will be positioned.
+ y - Y-coordinate on the base image where the top left corner of the target
+ will be positioned.
+ w - Width of the target.
+ h - Height of the target.
+
+A target specifies a place on the base image where a draggable can be
+positioned. By design, if the center of a draggable lies within the target
+(i.e. in the rectangle defined by [[x, y], [x + w, y + h]], then it is within
+the target. Otherwise, it is outside.
+
+If at lest one target is provided, the behavior of the client side logic
+changes. If a draggable is not dragged on to a target, it is returned back to
+the slider.
+
+If no targets are provided, then a draggable can be dragged and placed anywhere
+on the base image.
+
+correct answer format
+---------------------
+
+There are two correct answer formats: short and long
+If short from correct answer is mapping of 'draggable_id' to 'target_id'::
+
+ correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
+ correct_answer = {'name4': 't1', '7': 't2'}
+
+In long form correct answer is list of dicts. Every dict has 3 keys:
+draggables, targets and rule. For example::
+
+ correct_answer = [
+ {
+ 'draggables': ['7', '8'],
+ 'targets': ['t5_c', 't6_c'],
+ 'rule': 'anyof'
+ },
+ {
+ 'draggables': ['1', '2'],
+ 'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
+ 'rule': 'anyof'
+ }]
+
+Draggables is list of draggables id. Target is list of targets id, draggables
+must be dragged to with considering rule. Rule is string.
+
+Draggables in dicts inside correct_answer list must not intersect!!!
+
+Wrong (for draggable id 7)::
+
+ correct_answer = [
+ {
+ 'draggables': ['7', '8'],
+ 'targets': ['t5_c', 't6_c'],
+ 'rule': 'anyof'
+ },
+ {
+ 'draggables': ['7', '2'],
+ 'targets': ['t2_h', 't3_h', 't4_h', 't7_h', 't8_h', 't10_h'],
+ 'rule': 'anyof'
+ }]
+
+Rules are: exact, anyof, unordered_equal, anyof+number, unordered_equal+number
+
+
+.. such long lines are needed for sphinx to display lists correctly
+
+- Exact rule means that targets for draggable id's in user_answer are the same that targets from correct answer. For example, for draggables 7 and 8 user must drag 7 to target1 and 8 to target2 if correct_answer is::
+
+ correct_answer = [
+ {
+ 'draggables': ['7', '8'],
+ 'targets': ['tartget1', 'target2'],
+ 'rule': 'exact'
+ }]
+
+
+- unordered_equal rule allows draggables be dragged to targets unordered. If one want to allow for student to drag 7 to target1 or target2 and 8 to target2 or target 1 and 7 and 8 must be in different targets, then correct answer must be::
+
+ correct_answer = [
+ {
+ 'draggables': ['7', '8'],
+ 'targets': ['tartget1', 'target2'],
+ 'rule': 'unordered_equal'
+ }]
+
+
+- Anyof rule allows draggables to be dragged to any of targets. If one want to allow for student to drag 7 and 8 to target1 or target2, which means that if 7 is on target1 and 8 is on target1 or 7 on target2 and 8 on target2 or 7 on target1 and 8 on target2. Any of theese are correct which anyof rule::
+
+ correct_answer = [
+ {
+ 'draggables': ['7', '8'],
+ 'targets': ['tartget1', 'target2'],
+ 'rule': 'anyof'
+ }]
+
+
+- If you have can_reuse true, then you, for example, have draggables a,b,c and 10 targets. These will allow you to drag 4 'a' draggables to ['target1', 'target4', 'target7', 'target10'] , you do not need to write 'a' four times. Also this will allow you to drag 'b' draggable to target2 or target5 for target5 and target2 etc..::
+
+ correct_answer = [
+ {
+ 'draggables': ['a'],
+ 'targets': ['target1', 'target4', 'target7', 'target10'],
+ 'rule': 'unordered_equal'
+ },
+ {
+ 'draggables': ['b'],
+ 'targets': ['target2', 'target5', 'target8'],
+ 'rule': 'anyof'
+ },
+ {
+ 'draggables': ['c'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'unordered_equal'
+ }]
+
+- And sometimes you want to allow drag only two 'b' draggables, in these case you sould use 'anyof+number' of 'unordered_equal+number' rule::
+
+ correct_answer = [
+ {
+ 'draggables': ['a', 'a', 'a'],
+ 'targets': ['target1', 'target4', 'target7'],
+ 'rule': 'unordered_equal+numbers'
+ },
+ {
+ 'draggables': ['b', 'b'],
+ 'targets': ['target2', 'target5', 'target8'],
+ 'rule': 'anyof+numbers'
+ },
+ {
+ 'draggables': ['c'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'unordered_equal'
+ }]
+
+In case if we have no multiple draggables per targets (one_per_target="true"),
+for same number of draggables, anyof is equal to unordered_equal
+
+If we have can_reuse=true, than one must use only long form of correct answer.
+
+
+Grading logic
+-------------
+
+1. User answer (that comes from browser) and correct answer (from xml) are parsed to the same format::
+
+ group_id: group_draggables, group_targets, group_rule
+
+
+Group_id is ordinal number, for every dict in correct answer incremental
+group_id is assigned: 0, 1, 2, ...
+
+Draggables from user answer are added to same group_id where identical draggables
+from correct answer are, for example::
+
+ If correct_draggables[group_0] = [t1, t2] then
+ user_draggables[group_0] are all draggables t1 and t2 from user answer:
+ [t1] or [t1, t2] or [t1, t2, t2] etc..
+
+2. For every group from user answer, for that group draggables, if 'number' is in group rule, set() is applied,
+if 'number' is not in rule, set is not applied::
+
+ set() : [t1, t2, t3, t3] -> [t1, t2, ,t3]
+
+For every group, at this step, draggables lists are equal.
+
+
+3. For every group, lists of targets are compared using rule for that group.
+
+
+Set and '+number' cases
+.......................
+
+Set() and '+number' are needed only for case of reusable draggables,
+for other cases there are no equal draggables in list, so set() does nothing.
+
+.. such long lines needed for sphinx to display nicely
+
+* Usage of set() operation allows easily create rule for case of "any number of same draggable can be dragged to some targets"::
+
+ {
+ 'draggables': ['draggable_1'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'anyof'
+ }
+
+
+
+
+* 'number' rule is used for the case of reusable draggables, when one want to fix number of draggable to drag. In this example only two instances of draggables_1 are allowed to be dragged::
+
+ {
+ 'draggables': ['draggable_1', 'draggable_1'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'anyof+number'
+ }
+
+
+* Note, that in using rule 'exact', one does not need 'number', because you can't recognize from user interface which reusable draggable is on which target. Absurd example::
+
+ {
+ 'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'exact'
+ }
+
+
+ Correct handling of this example is to create different rules for draggable_1 and
+ draggable_2
+
+* For 'unordered_equal' (or 'exact' too) we don't need 'number' if you have only same draggable in group, as targets length will provide constraint for the number of draggables::
+
+ {
+ 'draggables': ['draggable_1'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'unordered_equal'
+ }
+
+
+ This means that only three draggaggables 'draggable_1' can be dragged.
+
+* But if you have more that one different reusable draggable in list, you may use 'number' rule::
+
+ {
+ 'draggables': ['draggable_1', 'draggable_1', 'draggable_2'],
+ 'targets': ['target3', 'target6', 'target9'],
+ 'rule': 'unordered_equal+number'
+ }
+
+
+ If not use number, draggables list will be setted to ['draggable_1', 'draggable_2']
+
+
+
+
+Logic flow
+----------
+
+(Click on image to see full size version.)
+
+.. image:: draganddrop_logic_flow.png
+ :width: 100%
+ :target: _images/draganddrop_logic_flow.png
+
+
+Example
+=======
+
+Examples of draggables that can't be reused
+-------------------------------------------
+
+.. literalinclude:: drag-n-drop-demo.xml
+
+Draggables can be reused
+------------------------
+
+.. literalinclude:: drag-n-drop-demo2.xml
diff --git a/docs/source/draganddrop_logic_flow.png b/docs/source/draganddrop_logic_flow.png
new file mode 100644
index 0000000000..2bb1c11a41
Binary files /dev/null and b/docs/source/draganddrop_logic_flow.png differ
diff --git a/docs/source/xml_formats.rst b/docs/source/xml_formats.rst
index b76ee11642..7c92546a5e 100644
--- a/docs/source/xml_formats.rst
+++ b/docs/source/xml_formats.rst
@@ -5,4 +5,5 @@ Contents:
.. toctree::
:maxdepth: 2
- graphical_slider_tool.rst
\ No newline at end of file
+ graphical_slider_tool.rst
+ drag_and_drop_input.rst
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 330c8fd304..570d26ae2a 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -432,10 +432,11 @@ courseware_only_js += [
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/modules/**/*.coffee')
]
+# 'js/vendor/RequireJS.js' - Require JS wrapper.
+# See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
main_vendor_js = [
'js/vendor/RequireJS.js',
'js/vendor/json2.js',
- 'js/vendor/RequireJS.js',
'js/vendor/jquery.min.js',
'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.cookie.js',
diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py
index bab0ab3691..c34156da52 100644
--- a/lms/lib/symmath/formula.py
+++ b/lms/lib/symmath/formula.py
@@ -154,8 +154,9 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
class formula(object):
'''
- Representation of a mathematical formula object. Accepts mathml math expression for constructing,
- and can produce sympy translation. The formula may or may not include an assignment (=).
+ Representation of a mathematical formula object. Accepts mathml math expression
+ for constructing, and can produce sympy translation. The formula may or may not
+ include an assignment (=).
'''
def __init__(self, expr, asciimath='', options=None):
self.expr = expr.strip()
@@ -194,8 +195,12 @@ class formula(object):
def preprocess_pmathml(self, xml):
'''
- Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also
- to accomodate some sympy conventions (eg hat(i) for \hat{i}).
+ Pre-process presentation MathML from ASCIIMathML to make it more
+ acceptable for SnuggleTeX, and also to accomodate some sympy
+ conventions (eg hat(i) for \hat{i}).
+
+ This method would be a good spot to look for an integral and convert
+ it, if possible...
'''
if type(xml) == str or type(xml) == unicode:
@@ -266,6 +271,9 @@ class formula(object):
'''
Return sympy expression for the math formula.
The math formula is converted to Content MathML then that is parsed.
+
+ This is a recursive function, called on every CMML node. Support for
+ more functions can be added by modifying opdict, abould halfway down
'''
if self.the_sympy: return self.the_sympy
diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py
index bcb4a0d490..3cc4fd7d3c 100644
--- a/lms/lib/symmath/symmath_check.py
+++ b/lms/lib/symmath/symmath_check.py
@@ -157,13 +157,33 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
'''
Check a symbolic mathematical expression using sympy.
The input may be presentation MathML. Uses formula.
+
+ This is the default Symbolic Response checking function
+
+ Desc of args:
+ expect is a sympy string representing the correct answer. It is interpreted
+ using my_sympify (from formula.py), which reads strings as sympy input
+ (e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5)
+
+ ans is student-typed answer. It is expected to be ascii math, but the code
+ below would support a sympy string.
+
+ dynamath is the PMathML string converted by MathJax. It is used if
+ evaluation with ans is not sufficient.
+
+ options is a string with these possible substrings, set as an xml property
+ of the problem:
+ -matrix - make a sympy matrix, rather than a list of lists, if possible
+ -qubit - passed to my_sympify
+ -imaginary - used in formla, presumably to signal to use i as sqrt(-1)?
+ -numerical - force numerical comparison.
'''
msg = ''
# msg += 'abname=%s' % abname
# msg += 'adict=%s' % (repr(adict).replace('<','<'))
- threshold = 1.0e-3
+ threshold = 1.0e-3 # for numerical comparison (also with matrices)
DEBUG = debug
if xml is not None:
@@ -184,13 +204,17 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
msg += '
Error %s in parsing OUR expected answer "%s"
' % (err, expect)
return {'ok': False, 'msg': make_error_message(msg)}
+
+ ###### Sympy input #######
# if expected answer is a number, try parsing provided answer as a number also
try:
fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
except Exception, err:
fans = None
- if hasattr(fexpect, 'is_number') and fexpect.is_number and fans and hasattr(fans, 'is_number') and fans.is_number:
+ # do a numerical comparison if both expected and answer are numbers
+ if (hasattr(fexpect, 'is_number') and fexpect.is_number and fans
+ and hasattr(fans, 'is_number') and fans.is_number):
if abs(abs(fans - fexpect) / fexpect) < threshold:
return {'ok': True, 'msg': msg}
else:
@@ -208,6 +232,8 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
msg += '
You entered: %s
' % to_latex(fans)
return {'ok': True, 'msg': msg}
+
+ ###### PMathML input ######
# convert mathml answer to formula
try:
if dynamath:
@@ -216,6 +242,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
mmlans = None
if not mmlans:
return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
+
f = formula(mmlans, options=options)
# get sympy representation of the formula
@@ -238,7 +265,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
msg += ''
return {'ok': False, 'msg': make_error_message(msg)}
- # compare with expected
+ # do numerical comparison with expected
if hasattr(fexpect, 'is_number') and fexpect.is_number:
if hasattr(fsym, 'is_number') and fsym.is_number:
if abs(abs(fsym - fexpect) / fexpect) < threshold:
@@ -250,6 +277,10 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
# msg += "
cmathml =
%s
" % str(f.cmathml).replace('<','<')
return {'ok': False, 'msg': make_error_message(msg)}
+ # Here is a good spot for adding calls to X.simplify() or X.expand(),
+ # allowing equivalence over binomial expansion or trig identities
+
+ # exactly the same?
if fexpect == fsym:
return {'ok': True, 'msg': msg}
diff --git a/lms/static/images/press/releases/dr-lewin-276_2400x1600.jpg.REMOVED.git-id b/lms/static/images/press/releases/dr-lewin-276_2400x1600.jpg.REMOVED.git-id
new file mode 100644
index 0000000000..7166a5027c
--- /dev/null
+++ b/lms/static/images/press/releases/dr-lewin-276_2400x1600.jpg.REMOVED.git-id
@@ -0,0 +1 @@
+b4d043bb1ca4a8815d4a388a2c9d96038211417b
\ No newline at end of file
diff --git a/lms/static/images/press/releases/dr-lewin-276_240x180.jpg b/lms/static/images/press/releases/dr-lewin-276_240x180.jpg
new file mode 100644
index 0000000000..d1a5300b1e
Binary files /dev/null and b/lms/static/images/press/releases/dr-lewin-276_240x180.jpg differ
diff --git a/lms/static/images/press/releases/dr-lewin-316_240x180.jpg b/lms/static/images/press/releases/dr-lewin-316_240x180.jpg
new file mode 100644
index 0000000000..6a659a3612
Binary files /dev/null and b/lms/static/images/press/releases/dr-lewin-316_240x180.jpg differ
diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss
index 415199141d..aa84e0ff52 100644
--- a/lms/templates/feed.rss
+++ b/lms/templates/feed.rss
@@ -6,16 +6,25 @@
##
EdX Blog
- 2012-12-19T14:00:12-07:00
+ 2013-01-21T14:00:12-07:00
- tag:www.edx.org,2012:Post/10
- 2012-12-19T14:00:00-07:00
- 2012-12-19T14:00:00-07:00
-
- edX announces first wave of new courses for Spring 2013
- <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
+ tag:www.edx.org,2012:Post/11
+ 2013-01-22T10:00:00-07:00
+ 2013-01-22T10:00:00-07:00
+
+ New course from legendary MIT physics professor Walter Lewin
+ <img src="${static.url('images/press/releases/dr-lewin-316_240x180.jpg')}" />
<p></p>
+
+
+
+
+
+
+
+
+
tag:www.edx.org,2012:Post/92012-12-10T14:00:00-07:00
diff --git a/lms/templates/static_templates/media-kit.html b/lms/templates/static_templates/media-kit.html
index 458cfb8e15..73eea9c3b8 100644
--- a/lms/templates/static_templates/media-kit.html
+++ b/lms/templates/static_templates/media-kit.html
@@ -89,7 +89,7 @@
- Screenshot of 6.00x: Introduction to Computer Science and Programming.
+ Screenshot of 3.091x: Introduction to Solid State Chemistry.Download (High Resolution Photo)
@@ -108,4 +108,4 @@
return false;
});
-%block>
\ No newline at end of file
+%block>
diff --git a/lms/templates/static_templates/press_releases/Lewin_course_announcement.html b/lms/templates/static_templates/press_releases/Lewin_course_announcement.html
new file mode 100644
index 0000000000..4fb2a2c83e
--- /dev/null
+++ b/lms/templates/static_templates/press_releases/Lewin_course_announcement.html
@@ -0,0 +1,77 @@
+<%! from django.core.urlresolvers import reverse %>
+<%inherit file="../../main.html" />
+
+<%namespace name='static' file='../../static_content.html'/>
+
+<%block name="title">New Course from legendary MIT physics professor Walter Lewin%block>
+
+
+
+
+
+
Afraid of physics? Do you hate it? Walter Lewin will make you love physics whether you like it or not
+
+
+
MIT physics professor and online web star brings his renowned Electricity and Magnetism course to edX
+
+
+
+
+
Walter Lewin, legendary MIT physics professor, demonstrates, in his inimitable fashion, one of the many laws of physics covered in his new course on edX.
CAMBRIDGE, MA – January 22, 2013 –EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today a new course from the legendary Professor Walter Lewin who, for 47 years, has provided generations of MIT students – and millions watching online – with his inspiring and unconventional lectures. Now, with this edX version of Professor Lewin’s famous course Electricity and Magnetism (Physics), people around the world can experience it just like his students on the MIT campus. MITx 8.02x Electricity and Magnetism is now open for enrollment and classes will begin on February 18, 2013.
+
+
“I have taught this course to tens of thousands and many tell me it changed their lives,” said Walter Lewin, Professor of Physics at MIT. “Teaching is my passion: I want to open peoples’ eyes and minds to the beauty of physics so they will begin to see the world in a new way.”
+
+
In 8.02x Electricity and Magnetism, Professor Lewin will teach students to “see” the world instead of just “looking at” it. He will make them “see” natural phenomena such as rainbows in a way they never imagined before. Through his dynamic teaching, enthusiasm and great sense of humor, Professor Lewin has an innate ability to make difficult concepts easy. The New York Times has crowned him a “Web Star” and noted how his lectures, with their engaging physics demonstrations, have won him devotees around the world. While this course is MIT level, edX and Professor Lewin encourage even senior high school students from around the world to watch his lectures and take the course.
+
+
“Walter Lewin is an international treasure,” said Anant Agarwal, President of edX. “His physics lectures on the MIT campus were already legendary before he put them online and they became an international sensation. We know edX learners will be awestruck by his provocative and enlightening course.”
+
+
In addition to the basic concepts of Electromagnetism, a vast variety of interesting topics are covered, including Lightning, Pacemakers, Electric Shock Treatment, Electrocardiograms, Metal Detectors, Musical Instruments, Magnetic Levitation, Bullet Trains, Electric Motors, Radios, TV, Car Coils, Superconductivity, Aurora Borealis, Rainbows, Radio Telescopes, Interferometers, Particle Accelerators such as the Large Hadron Collider, Mass Spectrometers, Red Sunsets, Blue Skies, Haloes around Sun and Moon, Color Perception, Doppler Effect and Big-Bang Cosmology.
+
+
Professor Lewin received his PhD in Nuclear Physics at the Technical University in Delft, the Netherlands in 1965. He joined the Physics faculty at MIT in 1966 and became a pioneer in the new field of X-ray Astronomy. His 105 online lectures are world-renowned and are viewed by nearly 2 million people annually. Professor Lewin has received five teaching awards and is the only MIT professor featured in "The Best 300 Professors" by The Princeton Review. He has co-authored with Warren Goldstein the book "For the Love of Physics" (Free Press, Simon & Schuster), which has been translated into 9 languages.
EdX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.