Skip to content

OpenDSA exercises using latest Khan Academy framework version.

Hosam Shahin edited this page Sep 5, 2015 · 3 revisions

Background

We currently use the Khan Academy exercise framework to drive most of our exercises. What we actually did was to take a snapshot of their infrastructure about two years ago, and make modifications that split us off from their development process. We now need to do something to move forward. The options are to use a new system, adopt KA's new Perseus tool, or reimplement our KA exercise framework integration. Initial investigation of Perseus does not look promising. So far, we can find no collection of exercises associated with Perseus in the way that the main framework contains a large body of exercises. This might mean that Perseus is just a graphical frontend for developing exercises that are stored in the original format.

We decided to have our fork of khan Academy exercises framework so that we can extend it to satisfy OpenDSA requirements. Changes were made with two main points in mind. First, to minimize the impact on OpenDSA existing exercises as much as possible. Second, to keep the new framework core system unchanged as much as we can so that we can continue pulling new features, bug fixes and enhancements from the original repository.

Challenges

The new framework uses requireJS to load JavaScript modules. During testing we found that loading other libraries like JSAV and CodeMirror using the ordinary way, in a script tag, will not work. We modified the framework to add support to load JSAV, CodeMirror modules using the same mechanism the framework uses to load other JavaScript modules. So whenever exercises needs a module it define it in data-require attribute in html tag. data-require attribute accept multiple space delimited values.

Another issue was that the earlier version of Khan Academy has support for summative exercises where exercise developer can mix other exercises into one summary exercise by referring to individual exercises' names. Unfortunately Khan Academy team decided to remove that support, further more they reviewed all their exercises to inline all the remaining summatives without duplication.

We reviewed OpenDSA Exercises folder and found many exercises relaying on this feature for example Sorting Chapter Summary Exercise. So we decided to reimplement this vital feature again, the idea was to load all the exercises (html files referred to in the summary exercise) and construct a multi-part exercise out of it, and hand it to the framework to continue working as normal. Each exercise in the produced muti-part version will preserve its weight as defined in the original summary exercise.

How to transform existing OpenDSA exercise?

There are 3 simple steps you have to do in order to transform any exercise to work using the new framework:

  • If the exercise require JSAV or CodeMirror, you have to add "jsav" or "codemirror" strings in data-require attribute as described earlier.
  • Remove all script tags under head element and add the following instead:
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.min.js"></script>
<script src="../../khan-exercises/local-only/main.js" ></script>
  • All JavaScript variable or function written in a script tag under body element should be moved under "vars" element in new "config" variable which is an anonymous function. The moved variables/functions should be defined in a global scope.

Example: Transform BubSortPRO.html to use new framework

Please pay attention to the direction between comments (<-- -->) to realize what was done in order to transform an exercise.

Before:

<!--
Bubble Sort mini-proficiency exercise.
Written by Gayathri Subramanian and Cliff Shaffer
-->

<!DOCTYPE html>
<html data-require="math">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Bubble Sort Proficiency Exercise</title>
  <script src="../../lib/jquery.min.js"></script>
  <script src="../../lib/jquery-ui.min.js"></script>
  <script src="../../JSAV/lib/jquery.transit.js"></script>
  <script src="../../JSAV/lib/raphael.js"></script>
  <script src="../../ODSAkhan-exercises/khan-exercise.js"></script>
  <script src="../../JSAV/build/JSAV-min.js"></script>
  <link rel="stylesheet" href="../../JSAV/css/JSAV.css" type="text/css" />
  <link rel="stylesheet" href="../../lib/odsaStyle-min.css" type="text/css" />
</head>

<body data-height="550" data-width="950">
<script>
var
  jsav,           // The JSAV object
  answerArr = [], // The (internal) array that stores the correct answer
  cloneArr = [],  // A copy of the (internal) array at the start of the exercise for reset
  jsavArr,        // The array that the user manipulates (JSAV object)
  userInput,      // Boolean: Tells us if user ever did anything
  isSelected,     // Boolean: True iff user has already clicked an array element
  selected_index; // Position that has been selected by user for swap
// Click event handler on the array
var clickHandler = function (index, e) {
  if (selected_index === -1) { // if nothing currently selected
    jsavArr.css(index, {"font-size": "130%"});
    selected_index = index;
  }
  else {
    jsavArr.swap(selected_index, index);
    jsavArr.css(index, {"font-size": "100%"});
    jsavArr.css(selected_index, {"font-size": "100%"});
    selected_index = -1;  // Reset to nothing selected
  }
  userInput = true;
};
// reset function definition
var f_reset = function () {
  jsavArr.clear();             // Re-initialize the displayed array object
  jsavArr = jsav.ds.array(cloneArr, {indexed: true, center: false});
  jsavArr.click(clickHandler); // Rebind click handler after reset
  userInput = false;
  selected_index = -1;         // Reset to nothing selected
};
// swap two values in array
var swap = function (arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
};
// Initialise the exercise
var initJSAV = function (arr_size) {
  var i, j;
  userInput = false;
  selected_index = -1;
  answerArr.length = 0; // Out with the old
  // Give random numbers in range 0..999
  for (i = 0; i < arr_size; i++) {
    answerArr[i] = Math.floor(Math.random() * 1000);
  }
  // Now make a copy
  cloneArr = answerArr.slice(0);
  jsav = new JSAV("jsav");
  jsav.recorded();
  jsavArr = jsav.ds.array(answerArr, {indexed: true, center: false});
  // Compute the correct Answer
  for (i = 0; i < arr_size - 1; i++) {
    if (answerArr[i] > answerArr[i + 1]) {
      swap(answerArr, i, i + 1);
    }
  }
  // Bind the clickHandler to handle click events on the array
  jsavArr.click(clickHandler);
  // Set up handler for reset button
  $("#reset").click(function () { f_reset(); });
};
// Check student's answer for correctness: User's array must match answer
var checkAnswer = function (arr_size) {
  var i;
  for (i = 0; i < arr_size; i++) {
    if (jsavArr.value(i) !== answerArr[i]) {
      return false;
    }
  }
  return true;
};
</script>

<div class="exercise">
  <div class="vars">
    <var id="arr_size">randRange(7, 9)</var>
    <var id="JSAV">initJSAV(arr_size)</var>
  </div>

  <div class="problems">
    <div> <!-- Supresses answer box -->
      <div class="question">
        <p>Your task in this exercise is to show the
           behavior for one iteration of the outer for loop of Bubble Sort.
           Simply click on entries in the array to swap them in the way
           that Bubble Sort would during its first pass.</p>
        <input id="reset" type="button" value="Reset" />
        <div id="jsav" class="jsavcanvas"></div>
      </div>
      <div class="solution" data-type="custom">
        <div class="guess">
          [userInput]
        </div>
        <div class="validator-function">
          if (!checkAnswer(arr_size) && !guess[0])
            return ""; // User did not click, and correct answer is not
                 // initial array state
          else return checkAnswer(arr_size);
        </div>
      </div>
      <div class="hints">
        <p>What does Bubble Sort do?</p>
        <p>Move from left to right through the array.</p>
        <p>At each position, if the value is greater than the value to
           its right, then swap them.</p>
      </div>
    </div>
  </div>
</div>
</body>
</html>

After Transformation:

<!--
Bubble Sort mini-proficiency exercise.
Written by Gayathri Subramanian and Cliff Shaffer
-->

<!DOCTYPE html>
<!-- jsav string added to load JSAV module before the exercise is loaded -->
<html data-require="jsav math"> 
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Bubble Sort Proficiency Exercise</title>
  <!-- Only require.js and main module are loaded here -->
  <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.min.js"></script>
  <script src="../../khan-exercises/local-only/main.js" ></script>
</head>

<body data-height="550" data-width="950">

<div class="exercise">
  <div class="vars">
    <!-- Add new config var and move all javascript code inside it -->
    <var id="config"> 
      (function(){ <!-- cerate anonymous function -->
          <!-- All variables and funcitons should be defined in global scope -->
          window.jsav;           // The JSAV object  
          window.answerArr = []; // The (internal) array that stores the correct answer
          window.cloneArr = [];  // A copy of the (internal) array at the start of the exercise for reset
          window.jsavArr;        // The array that the user manipulates (JSAV object)
          window.userInput;      // Boolean: Tells us if user ever did anything
          window.isSelected;     // Boolean: True iff user has already clicked an array element
          window.selected_index; // Position that has been selected by user for swap

          // Click event handler on the array
          window.clickHandler = function (index, e) {
            if (selected_index === -1) { // if nothing currently selected
              jsavArr.css(index, {"font-size": "130%"});
              selected_index = index;
            }
            else {
              jsavArr.swap(selected_index, index);
              jsavArr.css(index, {"font-size": "100%"});
              jsavArr.css(selected_index, {"font-size": "100%"});
              selected_index = -1;  // Reset to nothing selected
            }
            userInput = true;
          };

          // reset function definition
          window.f_reset = function () {
            jsavArr.clear();             // Re-initialize the displayed array object
            jsavArr = jsav.ds.array(cloneArr, {indexed: true, center: false});
            jsavArr.click(clickHandler); // Rebind click handler after reset
            userInput = false;
            selected_index = -1;         // Reset to nothing selected
          };

          // swap two values in array
          window.swap = function (arr, i, j) {
            var temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
          };

          // Initialise the exercise
          window.initJSAV = function (arr_size) {
            var i, j;
            userInput = false;
            selected_index = -1;

            answerArr.length = 0; // Out with the old
            // Give random numbers in range 0..999
            for (i = 0; i < arr_size; i++) {
              answerArr[i] = Math.floor(Math.random() * 1000);
            }

            // Now make a copy
            cloneArr = answerArr.slice(0);

            jsav = new JSAV("jsav");
            jsav.recorded();
            jsavArr = jsav.ds.array(answerArr, {indexed: true, center: false});

            // Compute the correct Answer
            for (i = 0; i < arr_size - 1; i++) {
              if (answerArr[i] > answerArr[i + 1]) {
                swap(answerArr, i, i + 1);
              }
            }

            // Bind the clickHandler to handle click events on the array
            jsavArr.click(clickHandler);
            // Set up handler for reset button
            $("#reset").click(function () { f_reset(); });
          };

          // Check student's answer for correctness: User's array must match answer
          window.checkAnswer = function (arr_size) {
            var i;
            for (i = 0; i < arr_size; i++) {
              if (jsavArr.value(i) !== answerArr[i]) {
                return false;
              }
            }
            return true;
          };      
        }())
    </var>

    <var id="arr_size">randRange(7, 9)</var>
    <var id="JSAV">initJSAV(arr_size)</var>
  </div>

  <div class="problems">
  <!-- This div should contain id attribute, for consistency the value 
  should be the same as the filename -->
    <div id="BubsortPRO"> 
      <div class="question">
        <p>Your task in this exercise is to show the
           behavior for one iteration of the outer for loop of Bubble Sort.
           Simply click on entries in the array to swap them in the way
           that Bubble Sort would during its first pass.</p>
        <input id="reset" type="button" value="Reset" />
        <div id="jsav" class="jsavcanvas"></div>
      </div>
      <div class="solution" data-type="custom">
        <div class="guess">
          [userInput]
        </div>
        <div class="validator-function">
          if (!checkAnswer(arr_size) && !guess[0])
            return ""; // User did not click, and correct answer is not
                 // initial array state
          else return checkAnswer(arr_size);
        </div>
      </div>
      <div class="hints">
        <p>What does Bubble Sort do?</p>
        <p>Move from left to right through the array.</p>
        <p>At each position, if the value is greater than the value to
           its right, then swap them.</p>
      </div>
    </div>
  </div>
</div>
</body>
</html>