Skip to content
This repository
Browse code

Split out most exercise interface code

There is no scenario in which this is not a bitch to review. Sorry.

TODO: make local mode work again
TODO: get rid of utils/hints.js and merge with exercise-package/interface.js

Test Plan:
Did problems in Addition 1 locally and took hints and saw buttons update and lost leaves.
  • Loading branch information...
commit cbbd22da129cb8007ff95ff49fed08456856fa93 1 parent 2921406
Ben Alpert spicyj authored

Showing 3 changed files with 487 additions and 531 deletions. Show diff stats Hide diff stats

  1. +1 1  exercises/khan-exercise.html
  2. +368 14 interface.js
  3. +118 516 khan-exercise.js
2  exercises/khan-exercise.html
@@ -23,7 +23,7 @@
23 23 <div id='hintsarea'></div>
24 24 </div>
25 25 <div id="answer_area_wrap"><div id="answer_area">
26   - <form id="answerform" action="/exercisedashboard" method="get" name="answerform">
  26 + <form id="answerform" name="answerform">
27 27 <input type="submit" style="position: absolute; left: -9999px; width: 1px; height: 1px">
28 28 <div id="answercontent" class="info-box">
29 29 <span id="examples-show">Acceptable formats</span>
382 interface.js
@@ -7,37 +7,386 @@
7 7 */
8 8 (function() {
9 9
  10 +_.extend(Exercises, {
  11 + khanExercisesUrlBase: "/khan-exercises/",
10 12
11   -// (khan-exercises in local mode will stub this out)
12   -// TODO(alpert): Is this icky?
13   -Exercises.getCurrentFramework = function() {
14   - return Exercises.practiceExercise.getFramework();
15   -};
  13 + getCurrentFramework: function() {
  14 + // TODO(alpert): Is this icky?
  15 + return Exercises.practiceExercise.getFramework();
  16 + }
  17 +
  18 + guessLog: undefined,
  19 + userActivityLog: undefined
  20 +});
  21 +
  22 +
  23 +var originalCheckAnswerText,
16 24
  25 + userExercise,
  26 + problemNum,
  27 +
  28 + attempts,
  29 + numHints,
  30 + hintsUsed,
  31 + lastAttemptOrHint;
17 32
18 33 $(Exercises)
19 34 .bind("problemTemplateRendered", problemTemplateRendered)
  35 + .bind("newProblem", newProblem)
  36 + .bind("hintUsed", hintUsed)
20 37 .bind("readyForNextProblem", readyForNextProblem)
21 38 .bind("warning", warning)
22 39 .bind("upcomingExercise", upcomingExercise)
23 40 .bind("gotoNextProblem", gotoNextProblem)
24 41 .bind("updateUserExercise", updateUserExercise)
25   - .bind("enableCheckAnswer", enableCheckAnswer)
26   - .bind("disableCheckAnswer", disableCheckAnswer)
27 42 .bind("clearExistingProblem", clearExistingProblem);
28 43
29 44
30 45 function problemTemplateRendered() {
  46 + // Setup appropriate img URLs
  47 + $("#issue-throbber").attr("src",
  48 + Exercises.khanExercisesUrlBase + "css/images/throbber.gif");
  49 +
  50 + // 'Check Answer' or 'Submit Answer'
  51 + originalCheckAnswerText = $("#check-answer-button").val();
  52 +
  53 + // Solution submission
  54 + $("#check-answer-button").click(handleCheckAnswer);
  55 + $("#answerform").submit(handleCheckAnswer);
  56 +
  57 + // Hint button
  58 + $("#hint").click(handleHint);
  59 +
  60 + // Next question button
  61 + $("#next-question-button").click(function() {
  62 + $(Exercises).trigger("gotoNextProblem");
  63 +
  64 + // Disable next question button until next time
  65 + // TODO(alpert): Why? Is blurring not enough?
  66 + $(this)
  67 + .attr("disabled", true)
  68 + .addClass("buttonDisabled");
  69 + });
  70 +
  71 + // If happy face is clicked, pass click on through.
  72 + $("#positive-reinforcement").click(function() {
  73 + $("#next-question-button").click();
  74 + });
  75 +
  76 + // Let users close the warning bar when appropriate
  77 + $("#warning-bar-close a").click(function(e) {
  78 + e.preventDefault();
  79 + $("#warning-bar").fadeOut("slow");
  80 + });
  81 +
  82 + // Scratchpad toggle
  83 + $("#scratchpad-show").click(function(e) {
  84 + e.preventDefault();
  85 + Khan.scratchpad.toggle();
  86 +
  87 + if (userExercise.user) {
  88 + window.localStorage["scratchpad:" + userExercise.user] =
  89 + Khan.scratchpad.isVisible();
  90 + }
  91 + });
  92 +
31 93 var framework = Exercises.getCurrentFramework();
32 94 if (framework === "perseus") {
  95 + // TODO(alpert)
33 96 } else if (framework === "khan-exercises") {
34 97 $(Khan).trigger("problemTemplateRendered");
35 98 }
36 99 }
37 100
  101 +function newProblem(e, data) {
  102 + Exercises.guessLog = [];
  103 + Exercises.userActivityLog = [];
  104 +
  105 + attempts = 0;
  106 + numHints = data.numHints;
  107 + hintsUsed = 0;
  108 + lastAttemptOrHint = new Date().getTime();
  109 +}
  110 +
  111 +
  112 +function handleCheckAnswer() {
  113 + var framework = Exercises.getCurrentFramework();
  114 +
  115 + // Stop if the form is already disabled and we're waiting for a response
  116 + // (in assessment mode, for instance).
  117 + if ($("#answercontent input").not("#hint, #next-question-button")
  118 + .is(":disabled")) {
  119 + return false;
  120 + }
  121 +
  122 + var score = Khan.scoreInput();
  123 +
  124 + // Stop if the user didn't enter a response
  125 + if (score.empty) {
  126 + return false;
  127 + }
  128 +
  129 + Exercises.guessLog.push(score.guess);
  130 + Exercises.userActivityLog.push([
  131 + score.correct ? "correct-activity" : "incorrect-activity",
  132 + JSON.stringify(score.guess), timeTaken]);
  133 +
  134 + // Update interface corresponding to correctness
  135 + if (Exercises.assessmentMode) {
  136 + disableCheckAnswer();
  137 + } else if (score.correct) {
  138 + // Correct answer, so show the next question button.
  139 + $("#check-answer-button").hide();
  140 + $("#next-question-button")
  141 + .removeAttr("disabled")
  142 + .removeClass("buttonDisabled")
  143 + .show()
  144 + .focus();
  145 + $("#positive-reinforcement").show();
  146 + } else {
  147 + // Wrong answer. Enable all the input elements
  148 + //$("#answercontent input").not("#hint")
  149 + // .removeAttr("disabled");
  150 +
  151 + $("#check-answer-button")
  152 + .parent() // .check-answer-wrapper makes shake behave
  153 + .effect("shake", {times: 3, distance: 5}, 480)
  154 + .val("Try Again");
  155 +
  156 + // Is this a message to be shown?
  157 + if (score.message != null) {
  158 + $("#check-answer-results > p").html(score.message).tmpl().show();
  159 + } else {
  160 + $("#check-answer-results > p").hide();
  161 + }
  162 +
  163 + if (framework === "perseus") {
  164 + // TODO(alpert)?
  165 + } else if (framework === "khan-exercises") {
  166 + Khan.refocusSolutionInput();
  167 + }
  168 + }
  169 +
  170 + if (score.correct) {
  171 + $(Exercises).trigger("problemDone");
  172 + }
  173 +
  174 + var curTime = new Date().getTime();
  175 + var timeTaken = Math.round((curTime - lastAttemptOrHint) / 1000);
  176 + lastAttemptOrHint = curTime;
  177 +
  178 + // Save the problem results to the server
  179 + var data = buildAttemptData(score.correct, ++attempts,
  180 + JSON.stringify(score.guess), timeTaken);
  181 +
  182 + request("problems/" + problemNum + "/attempt", data).then(function() {
  183 + // TODO: Save locally if offline
  184 + $(Khan).trigger("attemptSaved");
  185 + }, function(xhr) {
  186 + // Alert any listeners of the error before reload
  187 + $(Exercises).trigger("attemptError");
  188 +
  189 + if (xhr && xhr.readyState === 0) {
  190 + // This path gets called when there is a broken pipe during
  191 + // page unload- browser navigating away during ajax request
  192 + // See http://stackoverflow.com/a/1370383.
  193 + return;
  194 + }
  195 +
  196 + // Error during submit. Disable the page and ask users to
  197 + // reload in an attempt to get updated data.
  198 +
  199 + // Hide the page so users don't continue, then warn the user about the
  200 + // problem and encourage reloading the page
  201 + $("#problem-and-answer").css("visibility", "hidden");
  202 + $(Exercises).trigger("warning",
  203 + "This page is out of date. You need to <a href='" +
  204 + _.escape(window.location.href) + "'>refresh</a>, but " +
  205 + "don't worry, you haven't lost progress. If you think " +
  206 + "this is a mistake, <a href='http://www.khanacademy.org/" +
  207 + "reportissue?type=Defect&issue_labels='>tell us</a>.");
  208 + });
  209 +
  210 + $(Exercises).trigger("checkAnswer", {
  211 + correct: score.correct,
  212 +
  213 + // Determine if this attempt qualifies as fast completion
  214 + fast: (typeof userExercise !== "undefined" &&
  215 + userExercise.secondsPerFastProblem >= timeTaken)
  216 + });
  217 +
  218 + return false;
  219 +}
  220 +
  221 +function handleHint() {
  222 + var framework = Exercises.getCurrentFramework();
  223 +
  224 + if (framework === "perseus") {
  225 + } else if (framework === "khan-exercises") {
  226 + $(Khan).trigger("showHint");
  227 + }
  228 +}
  229 +
  230 +function hintUsed() {
  231 + // Grow the scratchpad to cover the new hint
  232 + Khan.scratchpad.resize();
  233 +
  234 + hintsUsed++;
  235 + var hintsLeft = numHints - hintsUsed;
  236 +
  237 + // TODO(alpert): Get rid of hints.js; it's silly
  238 + var $hintButton = $("#hint");
  239 + var stepsLeft = hintsLeft === 1 ? "1 hint left" :
  240 + hintsLeft + " hints left";
  241 + $hintButton.val($hintButton.data("buttonText") ||
  242 + "I'd like another hint (" + stepsLeft + ")");
  243 +
  244 + // If there aren't any more hints, disable the get hint button
  245 + if (hintsLeft === 0) {
  246 + $hintButton.attr("disabled", true);
  247 + $(Exercises).trigger("allHintsUsed");
  248 + }
  249 +
  250 + var curTime = new Date().getTime();
  251 + var timeTaken = Math.round((curTime - lastAttemptOrHint) / 1000);
  252 + lastAttemptOrHint = curTime;
  253 +
  254 + var answeredCorrectly = $("#next-question-button").is(":visible");
  255 + if (!userExercise.readOnly && !answeredCorrectly) {
  256 + Exercises.userActivityLog.push(["hint-activity", "0", timeTaken]);
  257 +
  258 + // Resets the streak and logs history for exercise viewer
  259 + // Don't do anything on success or failure; silently failing is ok here
  260 + request("problems/" + problemNum + "/hint",
  261 + buildAttemptData(false, attempts, "hint", timeTaken));
  262 + }
  263 +}
  264 +
  265 +// Build the data to pass to the server
  266 +function buildAttemptData(pass, attemptNum, attemptContent, timeTaken) {
  267 + var framework = Exercises.getCurrentFramework();
  268 + var data;
  269 +
  270 + if (framework === "perseus") {
  271 + // TODO(alpert)
  272 + data = {};
  273 + } else if (framework === "khan-exercises") {
  274 + data = _.extend({
  275 + complete: pass === true ? 1 : 0
  276 + }, Khan.getSeedInfo());
  277 + }
  278 +
  279 + _.extend(data, {
  280 + // Ask for camel casing in returned response
  281 + casing: "camel",
  282 +
  283 + // Whether we're moving to the next problem (i.e., correctness)
  284 + count_hints: hintsUsed,
  285 + time_taken: timeTaken,
  286 +
  287 + // How many times the problem was attempted
  288 + attempt_number: attemptNum,
  289 +
  290 + // The answer the user gave
  291 + attempt_content: attemptContent,
  292 +
  293 + // Whether we're currently in review mode
  294 + review_mode: Exercises.reviewMode ? 1 : 0,
  295 +
  296 + // Whether we are currently working on a topic, as opposed to an exercise
  297 + topic_mode: (!Exercises.reviewMode && !Exercises.practiceMode) ? 1 : 0,
  298 +
  299 + // The current card data
  300 + card: JSON.stringify(Exercises.currentCard),
  301 +
  302 + // Unique ID of the cached stack
  303 + stack_uid: Exercises.completeStack.getUid(),
  304 +
  305 + // The current topic, if any
  306 + topic_id: Exercises.topic && Exercises.topic.id,
  307 +
  308 + // How many cards the user has already done
  309 + cards_done: Exercises.completeStack.length,
  310 +
  311 + // How many cards the user has left to do
  312 + cards_left: Exercises.incompleteStack.length - 1,
  313 +
  314 + // Custom stack ID if it exists
  315 + custom_stack_id: Exercises.completeStack.getCustomStackID(),
  316 +
  317 + // The user assessment key if in assessmentMode
  318 + user_assessment_key: Exercises.userAssessmentKey
  319 + });
  320 +
  321 + return data;
  322 +}
  323 +
  324 +
  325 +var attemptHintQueue = jQuery({});
  326 +
  327 +// If there are any requests left in the queue when the window unloads then we
  328 +// will have permanently lost their answers and will need to clear the session
  329 +// cache, to make sure we don't override what is passed down from the servers
  330 +$(window).unload(function() {
  331 + if (attemptHintQueue.queue().length) {
  332 + $(Exercises).trigger("attemptError");
  333 + }
  334 +});
  335 +
  336 +function request(method, data) {
  337 + var apiBaseUrl = (Exercises.assessmentMode ?
  338 + "/api/v1/user/assessment/exercises" : "/api/v1/user/exercises");
  339 +
  340 + var params = {
  341 + // Do a request to the server API
  342 + url: apiBaseUrl + "/" + userExercise.exerciseModel.name + "/" + method,
  343 + type: "POST",
  344 + data: data,
  345 + dataType: "json"
  346 + };
  347 +
  348 + var deferred = $.Deferred();
  349 +
  350 + attemptHintQueue.queue(function(next) {
  351 + $.ajax(params).then(function(data, textStatus, jqXHR) {
  352 + deferred.resolve(data, textStatus, jqXHR);
  353 +
  354 + // Tell any listeners that we now have new userExercise data
  355 + $(Exercises).trigger("updateUserExercise", {
  356 + userExercise: data,
  357 + source: "serverResponse"
  358 + });
  359 + }, function(jqXHR, textStatus, errorThrown) {
  360 + // Execute passed error function first in case it wants different
  361 + // behavior depending upon the length of the request queue
  362 + // TODO(alpert): Huh? Don't think this matters.
  363 + deferred.reject(jqXHR, textStatus, errorThrown);
  364 +
  365 + // Clear the queue so we don't spit out a bunch of queued up
  366 + // requests after the error
  367 + attemptHintQueue.clearQueue();
  368 + }).always(function() {
  369 + $(Exercises).trigger("apiRequestEnded");
  370 + next();
  371 + });
  372 + });
  373 +
  374 + // Trigger an apiRequestStarted event here, and not in the queued function
  375 + // because listeners should know an API request is waiting as soon as it
  376 + // gets queued up.
  377 + $(Exercises).trigger("apiRequestStarted");
  378 +
  379 + return deferred.promise();
  380 +}
  381 +
  382 +
38 383 function readyForNextProblem(e, data) {
  384 + userExercise = data.userExercise;
  385 + problemNum = userExercise.totalDone + 1;
  386 +
39 387 var framework = Exercises.getCurrentFramework();
40 388 if (framework === "perseus") {
  389 + // TODO(alpert)
41 390 } else if (framework === "khan-exercises") {
42 391 $(Khan).trigger("readyForNextProblem", data);
43 392 }
@@ -60,8 +409,9 @@ function warning(e, message, showClose) {
60 409
61 410 function upcomingExercise(e, data) {
62 411 var framework = Exercises.getCurrentFramework();
63   - // TODO(alpert): Preload perseus problems in topic mode too
64   - if (framework === "khan-exercises") {
  412 + if (framework === "perseus") {
  413 + // TODO(alpert)
  414 + } else if (framework === "khan-exercises") {
65 415 $(Khan).trigger("upcomingExercise", data);
66 416 }
67 417 }
@@ -69,14 +419,18 @@ function upcomingExercise(e, data) {
69 419
70 420 function gotoNextProblem() {
71 421 var framework = Exercises.getCurrentFramework();
72   - if (framework === "khan-exercises") {
  422 + if (framework === "perseus") {
  423 + // TODO(alpert)
  424 + } else if (framework === "khan-exercises") {
73 425 $(Khan).trigger("gotoNextProblem");
74 426 }
75 427 }
76 428
77 429 function updateUserExercise(e, data) {
78 430 var framework = Exercises.getCurrentFramework();
79   - if (framework === "khan-exercises") {
  431 + if (framework === "perseus") {
  432 + // TODO(alpert)
  433 + } else if (framework === "khan-exercises") {
80 434 $(Khan).trigger("updateUserExercise", data);
81 435 }
82 436 }
@@ -111,11 +465,12 @@ function clearExistingProblem() {
111 465 $("#positive-reinforcement").hide();
112 466
113 467 // Wipe out any previous problem
114   - if (framework === "khan-exercises") {
  468 + if (framework === "perseus") {
  469 + // TODO(alpert): Do we have cleanup to do?
  470 + } else if (framework === "khan-exercises") {
115 471 $(Khan).trigger("cleanupProblem");
116 472 }
117 473 $("#workarea, #hintsarea").empty();
118   - $("#hint").attr("disabled", false);
119 474
120 475 // Take off the event handlers for disabling check answer; we'll rebind
121 476 // if we actually want them
@@ -124,5 +479,4 @@ function clearExistingProblem() {
124 479 Khan.scratchpad.clear();
125 480 }
126 481
127   -
128 482 })();
634 khan-exercise.js
@@ -134,7 +134,7 @@ var Khan = (function() {
134 134 return Math.abs(crc ^ (-1));
135 135 },
136 136
137   - userExercise = undefined,
  137 + userExercise,
138 138
139 139 // Check to see if we're in local mode
140 140 localMode = typeof Exercises === "undefined",
@@ -142,10 +142,6 @@ var Khan = (function() {
142 142 // Set in prepareSite when Exercises.init() has already been called
143 143 assessmentMode,
144 144
145   - // The main server we're connecting to for saving data
146   - server = typeof apiServer !== "undefined" ? apiServer :
147   - localMode ? "http://localhost:8080" : "",
148   -
149 145 // The ID, filename, and name of the exercise -- these will only be set here in localMode
150 146 exerciseId = ((/([^\/.]+)(?:\.html)?$/.exec(window.location.pathname) || [])[1]) || "",
151 147 exerciseFile = exerciseId + ".html",
@@ -202,8 +198,6 @@ var Khan = (function() {
202 198
203 199 // For saving problems to the server
204 200 hintsUsed,
205   - lastAction,
206   - attempts,
207 201
208 202 // Bug-hunting "undefined" attempt content
209 203 debugLogLog = ["start of log"],
@@ -211,12 +205,6 @@ var Khan = (function() {
211 205 debugLogLog.push(l);
212 206 },
213 207
214   - guessLog,
215   - userActivityLog,
216   -
217   - // A map of jQuery queues for serially sending and receiving AJAX requests.
218   - requestQueue = {},
219   -
220 208 // Dictionary of loading and loaded exercises; keys are exercise IDs,
221 209 // values are promises that are resolved when the exercise is loaded
222 210 exerciseFilePromises = {},
@@ -226,21 +214,15 @@ var Khan = (function() {
226 214 // site, immediately)
227 215 modulePromises = {},
228 216
229   - urlBase = typeof urlBaseOverride !== "undefined" ? urlBaseOverride :
230   - localMode ? "../" : "/khan-exercises/",
  217 + urlBase = localMode ? "../" : "/khan-exercises/",
231 218
232 219 // In local mode, we use khan-exercises local copy of the /images
233 220 // directory. But in production (on www.khanacademy.org), we use
234 221 // the canonical location of images, which is under '/'.
235   - imageBase = ((typeof urlBaseOverride !== "undefined" || localMode)
236   - ? (urlBase + "images/") : ("/images/"));
237   -
  222 + imageBase = localMode ? urlBase + "images/" : "/images/",
238 223
239 224 lastFocusedSolutionInput = null,
240 225
241   - // "Check answer" or in assessmentMode "Submit answer" - set in prepareSite
242   - originalCheckAnswerText = "",
243   -
244 226 issueError = "Communication with GitHub isn't working. Please file " +
245 227 "the issue manually at <a href=\"" +
246 228 "http://github.com/Khan/khan-exercises/issues/new\">GitHub</a>. " +
@@ -621,7 +603,13 @@ var Khan = (function() {
621 603 })).data("video", video);
622 604 },
623 605
624   - renderInSidebar: function() {
  606 + render: function() {
  607 + if (localMode) {
  608 + // Templates isn't available locally and we won't have any
  609 + // related videos to show anyway
  610 + return;
  611 + }
  612 +
625 613 var container = $(".related-video-box");
626 614 var jel = container.find(".related-video-list");
627 615 jel.empty();
@@ -669,20 +657,36 @@ var Khan = (function() {
669 657 {height: (isMouseEnter ? captionHeight : 0)},
670 658 options);
671 659 });
672   - },
673   -
674   - render: function() {
675   - // don't try to render if templates aren't present (dev mode)
676   - if (!window.Templates) return;
677   -
678   - this.renderInSidebar();
679 660 }
680 661 },
681 662
682 663 showSolutionButtonText: function() {
683 664 return hintsUsed ? "Show next step (" + hints.length + " left)" : "Show Solution";
684   - }
  665 + },
685 666
  667 + getSeedInfo: function() {
  668 + return {
  669 + // A hash representing the exercise version
  670 + sha1: typeof userExercise !== "undefined" ?
  671 + userExercise.exerciseModel.sha1 : exerciseId,
  672 + seed: problemSeed,
  673 + problem_type: problemID
  674 + };
  675 + },
  676 +
  677 + scoreInput: function() {
  678 + var guess = getAnswer();
  679 + var pass = validator(guess);
  680 + var empty = checkIfAnswerEmpty(guess) || checkIfAnswerEmpty(pass);
  681 +
  682 + // Really disentangling the true/false/""/"..." mess? Incroyable!
  683 + return {
  684 + empty: empty,
  685 + correct: pass === true,
  686 + message: typeof pass === "string" ? pass : null,
  687 + guess: guess
  688 + };
  689 + }
686 690 };
687 691 // see line 183. this ends the main Khan module
688 692
@@ -714,17 +718,6 @@ var Khan = (function() {
714 718 }
715 719
716 720 function onjQueryLoaded() {
717   - // If there are any requests left in the queue when the window unloads
718   - // then we will have permanently lost their answers and will need to
719   - // clear the session cache, to make sure we don't override what is
720   - // passed down from the servers
721   - $(window).unload(function() {
722   - if(requestQueue["attempt_hint_queue"] &&
723   - requestQueue["attempt_hint_queue"].queue().length) {
724   - $(Exercises).trigger("attemptError");
725   - }
726   - });
727   -
728 721 // Initialize to an empty jQuery set
729 722 exercises = $();
730 723
@@ -908,11 +901,6 @@ var Khan = (function() {
908 901 exerciseName = userExercise.exerciseModel.displayName;
909 902 exerciseFile = userExercise.exerciseModel.fileName;
910 903
911   - // TODO(eater): remove this once all of the exercises in the datastore have filename properties
912   - if (exerciseFile == null || exerciseFile == "") {
913   - exerciseFile = exerciseId + ".html";
914   - }
915   -
916 904 function finishRender() {
917 905 // Get all problems of this exercise type...
918 906 // TODO(alpert): What happens if multiple summatives in topic mode
@@ -938,7 +926,6 @@ var Khan = (function() {
938 926
939 927 // Generate a new problem
940 928 makeProblem(typeOverride, seedOverride);
941   -
942 929 }
943 930
944 931 startLoadingExercise(exerciseId, exerciseName, exerciseFile).then(
@@ -997,8 +984,10 @@ var Khan = (function() {
997 984
998 985
999 986 function checkIfAnswerEmpty(guess) {
1000   - return $.trim(guess) === "" ||
1001   - (guess instanceof Array && $.trim(guess.join("").replace(/,/g, "")) === "");
  987 + // If multiple-answer, join all responses and check if that's empty
  988 + // Remove commas left by joining nested arrays in case multiple-answer is nested
  989 + return $.trim(guess) === "" || (guess instanceof Array &&
  990 + $.trim(guess.join("").replace(/,/g, "")) === "");
1002 991 }
1003 992
1004 993 function makeProblem(id, seed) {
@@ -1210,8 +1199,6 @@ var Khan = (function() {
1210 1199 // Generate a type of problem
1211 1200 // (this includes possibly generating the multiple choice problems,
1212 1201 // if this fails then we will need to try generating another one.)
1213   - guessLog = [];
1214   - userActivityLog = [];
1215 1202 debugLog("decided on answer type " + answerType);
1216 1203 answerData = Khan.answerTypes[answerType].setup(solutionarea, solution);
1217 1204
@@ -1275,10 +1262,8 @@ var Khan = (function() {
1275 1262 // save a normal JS array of hints so we can shift() through them later
1276 1263 hints = hints.tmpl().children().get();
1277 1264
1278   - if (hints.length === 0) {
1279   - // Disable the get hint button
1280   - $("#hint").attr("disabled", true);
1281   - }
  1265 + // Enable/disable the get hint button
  1266 + $("#hint").attr("disabled", hints.length === 0);
1282 1267
1283 1268 // Hook out for exercise test runner
1284 1269 if (localMode && parent !== window && typeof parent.jQuery !== "undefined") {
@@ -1335,7 +1320,7 @@ var Khan = (function() {
1335 1320 height: "0",
1336 1321 "border-left": "6px solid transparent",
1337 1322 "border-right": "6px solid transparent",
1338   - position: "absolute",
  1323 + position: "absolute"
1339 1324 },
1340 1325
1341 1326 scrubber1 = $("<div>")
@@ -1365,8 +1350,7 @@ var Khan = (function() {
1365 1350 .data("hint", false)
1366 1351 .appendTo(timelineEvents);
1367 1352
1368   - var hintNumber = 0,
1369   - answerNumber = 1;
  1353 + var hintNumber = 0;
1370 1354
1371 1355 /* value[0]: css class
1372 1356 * value[1]: guess
@@ -1465,13 +1449,6 @@ var Khan = (function() {
1465 1449 var states = timelineEvents.children(".user-activity"),
1466 1450 currentSlide = Math.min(states.length - 1, 1),
1467 1451 numSlides = states.length,
1468   - firstHintIndex = timeline.find(".hint-activity:first")
1469   - .index(".user-activity"),
1470   - lastHintIndex = timeline.find(".hint-activity:last")
1471   - .index(".user-activity"),
1472   - totalHints = timeline.find(".hint-activity:last")
1473   - .index(".hint-activity"),
1474   - hintButton = $("#hint"),
1475 1452 timelineMiddle = timeline.width() / 2,
1476 1453 realHintsArea = $("#hintsarea"),
1477 1454 realWorkArea = $("#workarea"),
@@ -1570,7 +1547,7 @@ var Khan = (function() {
1570 1547 };
1571 1548
1572 1549 var activate = function(slideNum) {
1573   - var hint, thisState,
  1550 + var thisState,
1574 1551 thisSlide = states.eq(slideNum),
1575 1552 fadeTime = 150;
1576 1553
@@ -1620,7 +1597,7 @@ var Khan = (function() {
1620 1597
1621 1598 // Allow users to use arrow keys to move left and right in the
1622 1599 // timeline
1623   - $(document).keydown(function(event) {
  1600 + $(document).keydown(function() {
1624 1601 if (event.keyCode === 37) { // left
1625 1602 currentSlide -= 1;
1626 1603 } else if (event.keyCode === 39) { // right
@@ -1638,7 +1615,7 @@ var Khan = (function() {
1638 1615 });
1639 1616
1640 1617 // Allow users to click on points of the timeline
1641   - $(states).click(function(event) {
  1618 + $(states).click(function() {
1642 1619 var index = $(this).index("#timeline .user-activity");
1643 1620
1644 1621 currentSlide = index;
@@ -1647,7 +1624,7 @@ var Khan = (function() {
1647 1624 return false;
1648 1625 });
1649 1626
1650   - $("#previous-step").click(function(event) {
  1627 + $("#previous-step").click(function() {
1651 1628 if (currentSlide > 0) {
1652 1629 currentSlide -= 1;
1653 1630 activate(currentSlide);
@@ -1656,7 +1633,7 @@ var Khan = (function() {
1656 1633 return false;
1657 1634 });
1658 1635
1659   - $("#next-step").click(function(event) {
  1636 + $("#next-step").click(function() {
1660 1637 if (currentSlide < numSlides - 1) {
1661 1638 currentSlide += 1;
1662 1639 activate(currentSlide);
@@ -1665,11 +1642,11 @@ var Khan = (function() {
1665 1642 return false;
1666 1643 });
1667 1644
1668   - $("#next-problem").click(function(event) {
  1645 + $("#next-problem").click(function() {
1669 1646 window.location.href = userExercise.nextProblemUrl;
1670 1647 });
1671 1648
1672   - $("#previous-problem").click(function(event) {
  1649 + $("#previous-problem").click(function() {
1673 1650 if (!$(this).data("disabled")) {
1674 1651 window.location.href = userExercise.previousProblemUrl;
1675 1652 }
@@ -1721,12 +1698,15 @@ var Khan = (function() {
1721 1698
1722 1699 if (!Khan.query.activity) {
1723 1700 var historyURL = debugURL + "&seed=" + problemSeed + "&activity=";
1724   - $("<a>Problem history</a>").attr("href", "javascript:").click(function(event) {
1725   - window.location.href = historyURL + encodeURIComponent(JSON.stringify(userActivityLog));
  1701 + $("<a>Problem history</a>").attr("href", "javascript:").click(function() {
  1702 + window.location.href = historyURL + encodeURIComponent(
  1703 + JSON.stringify(Exercises.userActivityLog));
1726 1704 }).appendTo(links);
1727 1705 } else {
1728 1706 $("<a>Random problem</a>")
1729   - .attr("href", window.location.protocol + "//" + window.location.host + window.location.pathname + "?debug")
  1707 + .attr("href", window.location.protocol + "//" +
  1708 + window.location.host + window.location.pathname +
  1709 + "?debug")
1730 1710 .appendTo(links);
1731 1711 }
1732 1712
@@ -1828,12 +1808,12 @@ var Khan = (function() {
1828 1808 }
1829 1809
1830 1810 hintsUsed = 0;
1831   - attempts = 0;
1832   - lastAction = (new Date).getTime();
1833 1811
1834 1812 $("#hint").val("I'd like a hint");
1835 1813
1836   - $(Exercises).trigger("newProblem");
  1814 + $(Exercises).trigger("newProblem", {
  1815 + numHints: hints.length
  1816 + });
1837 1817
1838 1818 // If the textbox is empty disable "Check Answer" button
1839 1819 // Note: We don't do this for multiple choice, number line, etc.
@@ -1877,225 +1857,11 @@ var Khan = (function() {
1877 1857 }
1878 1858
1879 1859 function prepareSite() {
1880   - // TODO(david): Don't add homepage elements with "exercise" class
1881   - exercises = exercises.add($("div.exercise").detach());
1882   -
1883   - // Setup appropriate img URLs
1884   - $("#issue-throbber")
1885   - .attr("src", urlBase + "css/images/throbber.gif");
1886   -
1887   - // Change form target to the current page so errors do not kick us
1888   - // to the dashboard
1889   - $("#answerform").attr("action", window.location.href);
1890   -
1891   - // Watch for a solution submission
1892   - originalCheckAnswerText = $("#check-answer-button").val()
1893   - $("#check-answer-button").click(handleSubmit);
1894   - $("#answerform").submit(handleSubmit);
1895   -
1896 1860 // Grab example answer format container
1897 1861 examples = $("#examples");
1898 1862
1899 1863 assessmentMode = !localMode && Exercises.assessmentMode;
1900 1864
1901   - // Build the data to pass to the server
1902   - function buildAttemptData(pass, attemptNum, attemptContent, curTime) {
1903   - var timeTaken = Math.round((curTime - lastAction) / 1000);
1904   -
1905   - if (attemptContent !== "hint") {
1906   - userActivityLog.push([pass ? "correct-activity" : "incorrect-activity", attemptContent, timeTaken]);
1907   - } else {
1908   - userActivityLog.push(["hint-activity", "0", timeTaken]);
1909   - }
1910   -
1911   - return {
1912   - // The user answered correctly
1913   - complete: pass === true ? 1 : 0,
1914   -
1915   - // The user used a hint
1916   - count_hints: hintsUsed,
1917   -
1918   - // How long it took them to complete the problem
1919   - time_taken: timeTaken,
1920   -
1921   - // How many times the problem was attempted
1922   - attempt_number: attemptNum,
1923   -
1924   - // The answer the user gave
1925   - // TODO: Get the real provided answer
1926   - attempt_content: attemptContent,
1927   -
1928   - // A hash representing the exercise
1929   - // TODO: Populate this from somewhere
1930   - sha1: typeof userExercise !== "undefined" ? userExercise.exerciseModel.sha1 : exerciseId,
1931   -
1932   - // The seed that was used for generating the problem
1933   - seed: problemSeed,
1934   -
1935   - // The seed that was used for generating the problem
1936   - problem_type: problemID,
1937   -
1938   - // Whether we're currently in review mode
1939   - review_mode: (!localMode && Exercises.reviewMode) ? 1 : 0,
1940   -
1941   - // Whether we are currently working on a topic, as opposed to an exercise
1942   - topic_mode: (!localMode && !Exercises.reviewMode && !Exercises.practiceMode) ? 1 : 0,
1943   -
1944   - // Request camelCasing in returned response
1945   - casing: "camel",
1946   -
1947   - // The current card data
1948   - card: !localMode && JSON.stringify(Exercises.currentCard),
1949   -
1950   - // Unique ID of the cached stack
1951   - stack_uid: !localMode && Exercises.completeStack.getUid(),
1952   -
1953   - // The current topic, if any
1954   - topic_id: !localMode && Exercises.topic && Exercises.topic.id,
1955   -
1956   - // How many cards the user has already done
1957   - cards_done: !localMode && Exercises.completeStack.length,
1958   -
1959   - // How many cards the user has left to do
1960   - cards_left: !localMode && (Exercises.incompleteStack.length - 1),
1961   -
1962   - // Custom stack ID if it exists
1963   - custom_stack_id: !localMode && Exercises.completeStack.getCustomStackID(),
1964   -
1965   - // The user assessment key if in assessmentMode
1966   - user_assessment_key: !localMode && Exercises.userAssessmentKey
1967   - };
1968   - }
1969   -
1970   - function handleSubmit() {
1971   - var guess = getAnswer();
1972   - var pass = validator(guess);
1973   -
1974   - // Stop if the user didn't enter a response
1975   - // If multiple-answer, join all responses and check if that's empty
1976   - // Remove commas left by joining nested arrays in case multiple-answer is nested
1977   -
1978   - if (checkIfAnswerEmpty(guess) || checkIfAnswerEmpty(pass)) {
1979   - return false;
1980   - } else {
1981   - guessLog.push(guess);
1982   - }
1983   -
1984   - // Stop if the form is already disabled and we're waiting for a response.
1985   - if ($("#answercontent input").not("#hint,#next-question-button").is(":disabled")) {
1986   - return false;
1987   - }
1988   -
1989   - if(!assessmentMode) {
1990   - $("#answercontent input").not("#check-answer-button, #hint")
1991   - .attr("disabled", "disabled");
1992   - $("#check-answer-results p").hide();
1993   -
1994   - var checkAnswerButton = $("#check-answer-button");
1995   -
1996   - // If incorrect, warn the user and help them in any way we can
1997   - if (pass !== true) {
1998   - checkAnswerButton
1999   - .parent() // .check-answer-wrapper makes shake behave
2000   - .effect("shake", {times: 3, distance: 5}, 480)
2001   - .val("Try Again");
2002   -
2003   - // Is this a message to be shown?
2004   - if (typeof pass === "string") {
2005   - $("#check-answer-results .check-answer-message")
2006   - .html(pass).tmpl().show();
2007   - }
2008   -
2009   - // Refocus text field so user can type a new answer
2010   - if (lastFocusedSolutionInput != null) {
2011   - setTimeout(function() {
2012   - var focusInput = $(lastFocusedSolutionInput);
2013   -
2014   - if (!focusInput.is(":disabled")) {
2015   - // focus should always work; hopefully select
2016   - // will work for text fields
2017   - focusInput.focus();
2018   - if (focusInput.is("input:text")) {
2019   - focusInput.select();
2020   - }
2021   - }
2022   - }, 1);
2023   - }
2024   - }
2025   - }
2026   -
2027   - if (pass === true) {
2028   - // Problem has been completed but pending data request
2029   - // being sent to server.
2030   - $(Exercises).trigger("problemDone");
2031   - }
2032   -
2033   - // Save the problem results to the server
2034   - var curTime = new Date().getTime();
2035   - var data = buildAttemptData(pass, ++attempts, JSON.stringify(guess), curTime);
2036   - debugLog("attempt " + JSON.stringify(data));
2037   -
2038   - request("problems/" + problemNum + "/attempt", data, function() {
2039   -
2040   - // TODO: Save locally if offline
2041   - $(Khan).trigger("attemptSaved");
2042   -
2043   - }, function(xhr) {
2044   - // Alert any listeners of the error before reload
2045   - $(Exercises).trigger("attemptError");
2046   -
2047   - if (xhr && xhr.readyState == 0) {
2048   - // This path gets called when there is a broken pipe during
2049   - // page unload- browser navigating away during ajax request
2050   - // See http://stackoverflow.com/questions/1370322/jquery-ajax-fires-error-callback-on-window-unload
2051   - return;
2052   - }
2053   -
2054   - // Error during submit. Disable the page and ask users to
2055   - // reload in an attempt to get updated data.
2056   -
2057   - // Hide the page so users don't continue
2058   - $("#problem-and-answer").css("visibility", "hidden");
2059   -
2060   - // Warn user about problem, encourage to reload page
2061   - $(Exercises).trigger("warning",
2062   - "This page is out of date. You need to <a href='" +
2063   - _.escape(window.location.href) + "'>refresh</a>, but " +
2064   - "don't worry, you haven't lost progress. If you think " +
2065   - "this is a mistake, <a href='http://www.khanacademy.org/reportissue?type=Defect&issue_labels='>tell us</a>."
2066   - );
2067   -
2068   - }, "attempt_hint_queue");
2069   -
2070   - if (assessmentMode) {
2071   - $(Exercises).trigger("disableCheckAnswer");
2072   - } else if (pass === true) {
2073   - // Correct answer, so show the next question button.
2074   - $("#check-answer-button").hide();
2075   - $("#next-question-button")
2076   - .removeAttr("disabled")
2077   - .removeClass("buttonDisabled")
2078   - .show()
2079   - .focus();
2080   - $("#positive-reinforcement").show();
2081   - } else {
2082   - // Wrong answer. Enable all the input elements
2083   - $("#answercontent input").not("#hint")
2084   - .removeAttr("disabled");
2085   - }
2086   -
2087   - // Remember when the last action was
2088   - lastAction = curTime;
2089   -
2090   - $(Exercises).trigger("checkAnswer", {
2091   - pass: pass,
2092   - // Determine if this attempt qualifies as fast completion
2093   - fast: (typeof userExercise !== "undefined" && userExercise.secondsPerFastProblem >= data.time_taken)
2094   - });
2095   -
2096   - return false;
2097   - }
2098   -
2099 1865 function initializeCalculator() {
2100 1866 var calculator = $(".calculator"),
2101 1867 history = calculator.children(".history"),
@@ -2174,7 +1940,7 @@ var Khan = (function() {
2174 1940 return false;
2175 1941 });
2176 1942
2177   - $(Khan).on("gotoNextProblem", function(event) {
  1943 + $(Khan).on("gotoNextProblem", function() {
2178 1944 input.val("");
2179 1945 history.children().not(inputRow).remove();
2180 1946 });
@@ -2182,77 +1948,7 @@ var Khan = (function() {
2182 1948
2183 1949 initializeCalculator();
2184 1950
2185   - // Watch for when the next button is clicked
2186   - $("#next-question-button").click(function(ev) {
2187   - nextProblem(1);
2188   - $(Exercises).trigger("gotoNextProblem");
2189   -
2190   - // Disable next question button until next time
2191   - $(this)
2192   - .attr("disabled", true)
2193   - .addClass("buttonDisabled");
2194   - });
2195   -
2196   - // If happy face is clicked, pass click on through.
2197   - $("#positive-reinforcement").click(function() {
2198   - $("#next-question-button").click();
2199   - });
2200   -
2201   - // Watch for when the "Get a Hint" button is clicked
2202   - $("#hint").click(function() {
2203   -
2204   - var hint = hints.shift();
2205   -
2206   - if (hint) {
2207   - $(Exercises).trigger("hintUsed");
2208   -
2209   - hintsUsed += 1;
2210   -
2211   - var stepsLeft = hints.length + " step" + (hints.length === 1 ? "" : "s") + " left";
2212   - $(this).val($(this).data("buttonText") || "I'd like another hint (" + stepsLeft + ")");
2213   -
2214   - var problem = $(hint).parent();
2215   -
2216   - // Append first so MathJax can sense the surrounding CSS context properly
2217   - $(hint).appendTo("#hintsarea").runModules(problem);
2218   -
2219   - // Grow the scratchpad to cover the new hint
2220   - Khan.scratchpad.resize();
2221   -
2222   - // Disable the get hint button & add final_answer class
2223   - if (hints.length === 0) {
2224   - $(hint).addClass("final_answer");
2225   -
2226   - $(Exercises).trigger("allHintsUsed");
2227   -
2228   - $(this).attr("disabled", true);
2229   - }
2230   - }
2231   -
2232   - var fProdReadOnly = !localMode && userExercise.readOnly;
2233   - var fAnsweredCorrectly = $("#next-question-button").is(":visible");
2234   - if (!fProdReadOnly && !fAnsweredCorrectly) {
2235   - // Resets the streak and logs history for exercise viewer
2236   - request(
2237   - "problems/" + problemNum + "/hint",
2238   - buildAttemptData(false, attempts, "hint", new Date().getTime()),
2239   - // Don't do anything on success or failure, silently failing is ok here
2240   - function() {},
2241   - function() {},
2242   - "attempt_hint_queue"
2243   - );
2244   - }
2245   -
2246   - });
2247   -
2248   - // On an exercise page, replace the "Report a Problem" link with a button
2249   - // to be more clear that it won't replace the current page.
2250   - $("<a>Report a Problem</a>")
2251   - .attr("id", "report").addClass("simple-button green")
2252   - .replaceAll($(".footer-links #report"));
2253   -
2254 1951 $("#report").click(function(e) {
2255   -
2256 1952 e.preventDefault();
2257 1953
2258 1954 var report = $("#issue").css("display") !== "none",
@@ -2274,17 +1970,14 @@ var Khan = (function() {
2274 1970
2275 1971 // Hide issue form.
2276 1972 $("#issue-cancel").click(function(e) {
2277   -
2278 1973 e.preventDefault();
2279 1974
2280 1975 $("#issue").hide(500);
2281 1976 $("#issue-title, #issue-body").val("");
2282   -
2283 1977 });
2284 1978
2285 1979 // Submit an issue.
2286 1980 $("#issue form input:submit").click(function(e) {
2287   -
2288 1981 e.preventDefault();
2289 1982
2290 1983 // don't do anything if the user clicked a second time quickly
@@ -2296,14 +1989,14 @@ var Khan = (function() {
2296 1989 path = exerciseFile + "?seed=" +
2297 1990 problemSeed + "&problem=" + problemID,
2298 1991 pathlink = "[" + path + (exercise.data("name") !== exerciseId ? " (" + exercise.data("name") + ")" : "") + "](http://sandcastle.khanacademy.org/media/castles/Khan:master/exercises/" + path + "&debug)",
2299   - historyLink = "[Answer timeline](" + "http://sandcastle.khanacademy.org/media/castles/Khan:master/exercises/" + path + "&debug&activity=" + encodeURIComponent(JSON.stringify(userActivityLog)).replace(/\)/g, "\\)") + ")",
  1992 + historyLink = "[Answer timeline](" + "http://sandcastle.khanacademy.org/media/castles/Khan:master/exercises/" + path + "&debug&activity=" + encodeURIComponent(JSON.stringify(Exercises.userActivityLog)).replace(/\)/g, "\\)") + ")",
2300 1993 agent = navigator.userAgent,
2301 1994 mathjaxInfo = "MathJax is " + (typeof MathJax === "undefined" ? "NOT loaded" :
2302 1995 ("loaded, " + (MathJax.isReady ? "" : "NOT ") + "ready, queue length: " + MathJax.Hub.queue.queue.length)),
2303 1996 userHash = "User hash: " + crc32(user),
2304 1997 sessionStorageInfo = (typeof sessionStorage === "undefined" || typeof sessionStorage.getItem === "undefined" ? "sessionStorage NOT enabled" : null),
2305 1998 warningInfo = $("#warning-bar-content").text(),
2306   - parts = [$("#issue-body").val() || null, pathlink, historyLink, " " + JSON.stringify(guessLog), agent, sessionStorageInfo, mathjaxInfo, userHash, warningInfo],
  1999 + parts = [$("#issue-body").val() || null, pathlink, historyLink, " " + JSON.stringify(Exercises.guessLog), agent, sessionStorageInfo, mathjaxInfo, userHash, warningInfo],
2307 2000 body = $.grep(parts, function(e) { return e != null; }).join("\n\n");
2308 2001
2309 2002 var mathjaxLoadFailures = $.map(MathJax.Ajax.loading, function(info, script) {
@@ -2380,19 +2073,14 @@ var Khan = (function() {
2380 2073 labels: labels
2381 2074 };
2382 2075
2383   - // we try to post ot github without a cross-domain request, but if we're
2384   - // just running the exercises locally, then we can't help it and need
2385   - // to fall back to jsonp.
2386 2076 $.ajax({
2387   -
2388   - url: (localMode ? "http://www.khanacademy.org/" : "/") + "githubpost",
2389   - type: localMode ? "GET" : "POST",
2390   - data: localMode ? {json: JSON.stringify(dataObj)} :
2391   - JSON.stringify(dataObj),
2392   - contentType: localMode ? "application/x-www-form-urlencoded" : "application/json",
2393   - dataType: localMode ? "jsonp" : "json",
  2077 + url: "/githubpost",
  2078 + type: "POST",
  2079 + data: JSON.stringify(dataObj),
  2080 + contentType: "application/json",
  2081 + dataType: "json",
2394 2082 success: function(json) {
2395   -
  2083 + // TODO(alpert): Which is it?
2396 2084 var data = json.data || json;
2397 2085
2398 2086 // hide the form
@@ -2410,11 +2098,8 @@ var Khan = (function() {
2410 2098 // replace throbber with the cancel button
2411 2099 $("#issue-cancel").show();
2412 2100 $("#issue-throbber").hide();
2413   -
2414 2101 },
2415   - // note this won't actually work in local jsonp-mode
2416   - error: function(json) {
2417   -
  2102 + error: function() {
2418 2103 // show status message
2419 2104 $("#issue-status").addClass("error")
2420 2105 .html(issueError).show();
@@ -2425,26 +2110,10 @@ var Khan = (function() {
2425 2110 // replace throbber with the cancel button
2426 2111 $("#issue-cancel").show();
2427 2112 $("#issue-throbber").hide();
2428   -
2429 2113 }
2430 2114 });
2431 2115 });
2432 2116
2433   - $("#warning-bar-close a").click(function(e) {
2434   - e.preventDefault();
2435   - $("#warning-bar").fadeOut("slow");
2436   - });
2437   -
2438   - $("#scratchpad-show")
2439   - .click(function(e) {
2440   - e.preventDefault();
2441   - Khan.scratchpad.toggle();
2442   -
2443   - if (user) {
2444   - window.localStorage["scratchpad:" + user] = Khan.scratchpad.isVisible();
2445   - }
2446   - });
2447   -
2448 2117 $("#answer_area").delegate("input.button, select", "keydown", function(e) {
2449 2118 // Don't want to go back to exercise dashboard; just do nothing on backspace
2450 2119 if (e.keyCode === 8) {
@@ -2464,7 +2133,9 @@ var Khan = (function() {
2464 2133
2465 2134 $(Khan)
2466 2135 .bind("updateUserExercise", function(ev, data) {
2467   - // Any time we update userExercise, check if we're setting/switching usernames
  2136 + // TODO(alpert): Why isn't this in setUserExercise?
  2137 + // Any time we update userExercise, check if we're
  2138 + // setting/switching usernames
2468 2139 if (data && data.userExercise) {
2469 2140 user = data.userExercise.user || user;
2470 2141 userCRC32 = user != null ? crc32(user) : null;
@@ -2472,14 +2143,17 @@ var Khan = (function() {
2472 2143 }
2473 2144 });
2474 2145
2475   - // Register localMode-specific event handlers