From 04fee1c00199a86cafefebd12a5db0a74e8b7c8f Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sat, 16 Jul 2011 19:47:46 -0700 Subject: [PATCH 01/13] Display better suggestions for next problem to tackle, as mdeboard proposed. --- src/foreclojure/problems.clj | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index b68f4e52..840fef46 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -31,17 +31,37 @@ :where criteria :sort {:_id 1})))) -(defn next-unsolved-problem [solved-problems] - (when-let [unsolved (->> (get-problem-list) - (remove (comp (set solved-problems) :_id)) - (seq))] - (apply min-key :_id unsolved))) +(defn next-unsolved-problem [solved-problems just-solved-id] + (when-let [unsolved (seq + (from-mongo + (fetch :problems + :only [:_id :title] + :where {:_id {:$nin solved-problems}} + :sort {:_id 1})))] + (let [[skipped not-yet-tried] (split-with #(< (:_id %) just-solved-id) + unsolved)] + (filter identity [(rand-nth (or (seq skipped) + [nil])) ; rand-nth barfs on empty seq + (first not-yet-tried)])))) + +(letfn [(problem-link [{id :_id title :title}] + (str "" title ""))] + (defmulti suggest-problems count) + + (defmethod suggest-problems 0 [_] + "You've solved them all! Come back later for more!") + + (defmethod suggest-problems 1 [[problem]] + (str "Now try " (problem-link problem) "!")) + + (defmethod suggest-problems 2 [[skipped not-tried]] + (str "Now move on to " (problem-link not-tried) + ", or go back and try " (problem-link skipped) " again!"))) (defn next-problem-link [completed-problem-id] (when-let [{:keys [solved]} (get-user (session/session-get :user))] - (if-let [{:keys [_id title]} (next-unsolved-problem solved)] - (str "Now try " title "!") - "You've solved them all! Come back later for more!"))) + (suggest-problems + (next-unsolved-problem solved completed-problem-id)))) (defn get-recent-problems [n] (map get-problem (map :_id (take-last n (get-problem-list))))) From 1153525b0fa95a00f122a4f334b6b97f2577def0 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sat, 16 Jul 2011 20:22:13 -0700 Subject: [PATCH 02/13] Multimethods are great, but the previous definition was a total abuse. Refactor to use multiple function bodies instead. --- src/foreclojure/problems.clj | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index 840fef46..05c7f495 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -46,22 +46,18 @@ (letfn [(problem-link [{id :_id title :title}] (str "" title ""))] - (defmulti suggest-problems count) - - (defmethod suggest-problems 0 [_] - "You've solved them all! Come back later for more!") - - (defmethod suggest-problems 1 [[problem]] - (str "Now try " (problem-link problem) "!")) - - (defmethod suggest-problems 2 [[skipped not-tried]] - (str "Now move on to " (problem-link not-tried) - ", or go back and try " (problem-link skipped) " again!"))) + (defn suggest-problems + ([] "You've solved them all! Come back later for more!") + ([problem] + (str "Now try " (problem-link problem) "!")) + ([skipped not-tried] + (str "Now move on to " (problem-link not-tried) + ", or go back and try " (problem-link skipped) " again!")))) (defn next-problem-link [completed-problem-id] (when-let [{:keys [solved]} (get-user (session/session-get :user))] - (suggest-problems - (next-unsolved-problem solved completed-problem-id)))) + (apply suggest-problems + (next-unsolved-problem solved completed-problem-id)))) (defn get-recent-problems [n] (map get-problem (map :_id (take-last n (get-problem-list))))) From 906fdcd9996ccce87d5eb3fefb93496bf826f071 Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Sun, 17 Jul 2011 17:01:12 +0200 Subject: [PATCH 03/13] Slow down the AJAX interface a little bit When pressing the "Run" Button, the unit tests are executed a little too fast with little visual clues, which can have disorienting effects. This makes the green/red light(s) light up with 500ms delays, one after the other. The updated Message is displayed when the light of last test xor the failing test is updated. Please see the discussion here: https://github.com/dbyrne/4clojure/pull/83 --- resources/public/script/foreclojure.js | 55 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/resources/public/script/foreclojure.js b/resources/public/script/foreclojure.js index 523de2d0..76d17bea 100644 --- a/resources/public/script/foreclojure.js +++ b/resources/public/script/foreclojure.js @@ -60,6 +60,13 @@ function configureDataTables(){ } ); } +function setIconColor(element, color, timeOut) { + timeOut = (typeof timeOut == "undefined") ? 0 : timeOut + setTimeout (function() { + element.src = "/images/"+color+"light.png"; + }, timeOut); +} + function configureCodeBox(){ //For no javascript version we have the code-box text area //If we have javascript on then we remove it and replace it with @@ -81,41 +88,53 @@ function configureCodeBox(){ images = $(".testcases").find("img"), cont = true, high = false, - time = 800, + animationTime = 800, + waitTimePerItem = 500, + waitTime = waitTimePerItem, beforeSendCallback = function(data) { $("#message-text").text("Executing unit tests..."); + images.each( function(index, element) { + setIconColor(element, "blue"); + }); var anim = function() { if(cont) { images.animate({ opacity: high ? 1.0 : 0.1, - }, time); + }, animationTime); high = !high; - setTimeout(anim,time); + setTimeout(anim,animationTime); } }; anim(); }, successCallback = function(data) { - var failingTest = data.failingTest; - cont = false; + var failingTest = data.failingTest + getColorFor = function(index) { + return index === failingTest ? "red" : "green"; + }, + testWasExecuted = function(index) { + return index <= failingTest; + }, + setColor = function(index,element) { + var color = getColorFor(index); + waitTime = waitTimePerItem * (index+1); + setIconColor(element, color, waitTime); + }, + setMessages = function() { + $("#message-text").html(data.message); + $("#golfgraph").html(data.golfChart); + $("#golfscore").html(data.golfScore); + configureGolf(); + }; + cont = false; images.stop(true); images.css({ opacity: 1.0, }); - images.each( function(index,element) { - var color = "blue"; - if (index < failingTest) { - color = "green"; - } else if (index === failingTest) { - color = "red"; - } - element.src = "/images/"+color+"light.png"; - }); + images.filter( testWasExecuted ). + each(setColor); - $("#message-text").html(data.message); - $("#golfgraph").html(data.golfChart); - $("#golfscore").html(data.golfScore); - configureGolf(); + setTimeout (setMessages, waitTime); }; $.ajax({type: "POST", From a46f1cc8bb7e2b8f67c779e1cba853007e3fae8a Mon Sep 17 00:00:00 2001 From: Martin Sander Date: Sun, 17 Jul 2011 17:21:38 +0200 Subject: [PATCH 04/13] Small fix for the animation The pulsating animation was stopped when the request returns, while the icons where only updated a "waitTime" later. This fixes that. --- resources/public/script/foreclojure.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/public/script/foreclojure.js b/resources/public/script/foreclojure.js index 76d17bea..0f925b6e 100644 --- a/resources/public/script/foreclojure.js +++ b/resources/public/script/foreclojure.js @@ -126,14 +126,16 @@ function configureCodeBox(){ $("#golfgraph").html(data.golfChart); $("#golfscore").html(data.golfScore); configureGolf(); + } + stopAnimation = function() { + cont = false; + images.stop(true); + images.css({ opacity: 1.0, }); }; - cont = false; - images.stop(true); - images.css({ opacity: 1.0, }); + setTimeout(stopAnimation, waitTime); images.filter( testWasExecuted ). each(setColor); - setTimeout (setMessages, waitTime); }; From 4e1c7583e0415d5adefbb4042141aafa91e6af81 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sun, 17 Jul 2011 16:55:39 -0700 Subject: [PATCH 05/13] Jump straight to the interesting bits when displaying a problem --- src/foreclojure/problems.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index 05c7f495..0afd6516 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -45,7 +45,7 @@ (first not-yet-tried)])))) (letfn [(problem-link [{id :_id title :title}] - (str "" title ""))] + (str "" title ""))] (defn suggest-problems ([] "You've solved them all! Come back later for more!") ([problem] @@ -177,7 +177,8 @@ [(.getMessage e) *url*])))) (defn static-run-code [id raw-code] - (apply flash-msg (run-code id raw-code))) + (binding [*url* (str *url* "#prob-desc")] + (apply flash-msg (run-code id raw-code)))) (defn render-test-cases [tests] [:table {:class "testcases"} From 8444044f014d4df445d78e6e9d9fc86da5f42da8 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sun, 17 Jul 2011 18:40:28 -0700 Subject: [PATCH 06/13] Whitespace --- src/foreclojure/problems.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index 0afd6516..6a1878e0 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -138,7 +138,7 @@ :else (str "You've solved the problem! If you " (login-link "log in" (str "/problem/" _id)) " we can track your progress."))] (session/session-put! :code [_id code]) - [(str message " " gist-link) (str "/problem/" _id)] )) + [(str message " " gist-link) (str "/problem/" _id)])) (def restricted-list '[use require in-ns future agent send send-off pmap pcalls]) From 7e6e82b015c708396bab8e8a00906f7bd7141d03 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sun, 17 Jul 2011 18:42:23 -0700 Subject: [PATCH 07/13] Make run-code side-effect-less, so that ajax and static can both insert only the side effects they need --- src/foreclojure/problems.clj | 59 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index 6a1878e0..4dd2d5f4 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -154,31 +154,40 @@ (doall (take-while (complement #{end}) (repeatedly #(read *in* false end)))))))) -(defn run-code [id raw-code] - (let [code (.trim raw-code) - {:keys [tests restricted] :as problem} (get-problem id) - sb-tester (get-tester restricted)] - (session/flash-put! :code code) - (try - (let [user-forms (s/join " " (map pr-str (read-string-safely code)))] - (if (empty? user-forms) - ["Empty input is not allowed" *url*] - (loop [[test & more] tests - i 0] - (session/flash-put! :failing-test i) - (if-not test - (mark-completed problem code) - (let [testcase (s/replace test "__" user-forms)] - (if (sb sb-tester (first (read-string-safely testcase))) - (recur more (inc i)) - ["You failed the unit tests." *url*] - )))))) - (catch Exception e - [(.getMessage e) *url*])))) +(defn run-code + "Run the specified code-string against the test cases for the problem with the +specified id. + +Return a vector of [message-to-display url-to-display-at num-tests-passed]." + [id raw-code] + (try + (let [code (.trim raw-code) + {:keys [tests restricted] :as problem} (get-problem id) + sb-tester (get-tester restricted) + user-forms (s/join " " (map pr-str (read-string-safely code))) + results (if (empty? user-forms) + ["Empty input is not allowed."] + (for [test tests] + (try + (when-not (->> user-forms + (s/replace test "__") + read-string-safely + first + (sb sb-tester)) + "You failed the unit tests") + (catch Throwable t (.getMessage t))))) + [passed [fail-msg]] (split-with nil? results)] + (conj (if fail-msg + [fail-msg *url*] + (mark-completed problem code)) + (count passed))) + (catch Throwable t [(.getMessage t), *url*, 0]))) (defn static-run-code [id raw-code] - (binding [*url* (str *url* "#prob-desc")] - (apply flash-msg (run-code id raw-code)))) + (let [[message url failure-index] (binding [*url* (str *url* "#prob-desc")] + (run-code id raw-code))] + (session/flash-put! :failing-test failure-index) + (flash-msg message url))) (defn render-test-cases [tests] [:table {:class "testcases"} @@ -214,8 +223,8 @@ [:span#graph-link "View Chart"]]]))) (defn rest-run-code [id raw-code] - (let [[message url] (run-code id raw-code)] - (json-str {:failingTest (session/flash-get :failing-test) + (let [[message url failure-index] (run-code id raw-code)] + (json-str {:failingTest failure-index :message message :golfScore (html (render-golf-score)) :golfChart (html (render-golf-chart))}))) From 249c2fe62dd00e627d5445832c68ac01425816c2 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sun, 17 Jul 2011 18:49:02 -0700 Subject: [PATCH 08/13] Make run-code and mark-completed return maps instead of vectors, since the structure is getting complicated. --- src/foreclojure/problems.clj | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/foreclojure/problems.clj b/src/foreclojure/problems.clj index 4dd2d5f4..7e47af16 100644 --- a/src/foreclojure/problems.clj +++ b/src/foreclojure/problems.clj @@ -138,7 +138,7 @@ :else (str "You've solved the problem! If you " (login-link "log in" (str "/problem/" _id)) " we can track your progress."))] (session/session-put! :code [_id code]) - [(str message " " gist-link) (str "/problem/" _id)])) + {:message (str message " " gist-link), :url (str "/problem/" _id)})) (def restricted-list '[use require in-ns future agent send send-off pmap pcalls]) @@ -158,7 +158,7 @@ "Run the specified code-string against the test cases for the problem with the specified id. -Return a vector of [message-to-display url-to-display-at num-tests-passed]." +Return a map, {:message, :url, :num-tests-passed}." [id raw-code] (try (let [code (.trim raw-code) @@ -177,16 +177,18 @@ Return a vector of [message-to-display url-to-display-at num-tests-passed]." "You failed the unit tests") (catch Throwable t (.getMessage t))))) [passed [fail-msg]] (split-with nil? results)] - (conj (if fail-msg - [fail-msg *url*] - (mark-completed problem code)) - (count passed))) - (catch Throwable t [(.getMessage t), *url*, 0]))) + (assoc (if fail-msg + {:message fail-msg :url *url*} + (mark-completed problem code)) + :num-tests-passed (count passed))) + (catch Throwable t {:message (.getMessage t), :url *url* + :num-tests-passed 0}))) (defn static-run-code [id raw-code] - (let [[message url failure-index] (binding [*url* (str *url* "#prob-desc")] - (run-code id raw-code))] - (session/flash-put! :failing-test failure-index) + (let [{:keys [message url num-tests-passed]} + (binding [*url* (str *url* "#prob-desc")] + (run-code id raw-code))] + (session/flash-put! :failing-test num-tests-passed) (flash-msg message url))) (defn render-test-cases [tests] @@ -223,8 +225,8 @@ Return a vector of [message-to-display url-to-display-at num-tests-passed]." [:span#graph-link "View Chart"]]]))) (defn rest-run-code [id raw-code] - (let [[message url failure-index] (run-code id raw-code)] - (json-str {:failingTest failure-index + (let [{:keys [message url num-tests-passed]} (run-code id raw-code)] + (json-str {:failingTest num-tests-passed :message message :golfScore (html (render-golf-score)) :golfChart (html (render-golf-chart))}))) From 35e71b10c4dc031577772fa76f3974be3fb38ede Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Sun, 17 Jul 2011 20:57:15 -0700 Subject: [PATCH 09/13] We don't care about 4clojure.com/problems/ vs 4clojure.com/problems, and currently the former produces a 404, so standardize on the latter when a request comes in --- src/foreclojure/core.clj | 1 + src/foreclojure/ring.clj | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/foreclojure/core.clj b/src/foreclojure/core.clj index b9d442d7..e30a8626 100644 --- a/src/foreclojure/core.clj +++ b/src/foreclojure/core.clj @@ -34,6 +34,7 @@ session/wrap-stateful-session handler/site wrap-uri-binding + wrap-strip-trailing-slash wrap-gzip)) (defn run [] diff --git a/src/foreclojure/ring.clj b/src/foreclojure/ring.clj index b53e01a4..da087dd4 100644 --- a/src/foreclojure/ring.clj +++ b/src/foreclojure/ring.clj @@ -1,7 +1,8 @@ (ns foreclojure.ring (:use [compojure.core :only [GET]] [ring.util.response :only [response]]) - (:require [clojure.java.io :as io]) + (:require [clojure.java.io :as io] + [clojure.string :as s]) (:import (java.net URL))) ;; copied from compojure.route, modified to use File instead of Stream @@ -22,3 +23,7 @@ (= "file" (.getProtocol ^URL body))) (update-in resp [:body] io/as-file) resp)))) + +(defn wrap-strip-trailing-slash [handler] + (fn [request] + (handler (update-in request [:uri] s/replace #"/$" "")))) From b877f3d51eeb8f18715b472371344a89f6e7b2be Mon Sep 17 00:00:00 2001 From: David Byrne Date: Mon, 18 Jul 2011 06:32:06 -0700 Subject: [PATCH 10/13] Edited README.md via GitHub --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 93021ab5..63d214c2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ vaguely like this: * [David Davis](https://github.com/daviddavis) (daviddavis) * [Devin Walters](https://github.com/devn) (devn) * [Michael Kohl](https://github.com/citizen428) (citizen428) +* [Martin Sander](https://github.com/marvinthepa) (0x89) Problem sources: From bcfefe67804f439697154953fe52fee7caa66d86 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Mon, 18 Jul 2011 20:45:25 -0700 Subject: [PATCH 11/13] My strip-trailing-slashes broke the front page, by replacing '/' with '' for the root. --- src/foreclojure/ring.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foreclojure/ring.clj b/src/foreclojure/ring.clj index da087dd4..080d9b73 100644 --- a/src/foreclojure/ring.clj +++ b/src/foreclojure/ring.clj @@ -26,4 +26,4 @@ (defn wrap-strip-trailing-slash [handler] (fn [request] - (handler (update-in request [:uri] s/replace #"/$" "")))) + (handler (update-in request [:uri] s/replace #"(?<=.)/$" "")))) From 1e916670aa3c4d374bcf72ca3ae9350b1abe816a Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Mon, 11 Jul 2011 18:17:10 -0700 Subject: [PATCH 12/13] Add email address to the Contact Us text --- src/foreclojure/utils.clj | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/foreclojure/utils.clj b/src/foreclojure/utils.clj index 795fbd32..f24e100d 100644 --- a/src/foreclojure/utils.clj +++ b/src/foreclojure/utils.clj @@ -117,10 +117,10 @@ (defn html-doc [& body] (let [user (session/session-get :user)] - (html + (html (doctype :html5) - [:html - [:head + [:html + [:head [:title "4Clojure"] [:link {:rel "alternate" :type "application/atom+xml" :title "Atom" :href "http://4clojure.com/problems/rss"}] [:link {:rel "shortcut icon" :href "/favicon.ico"}] @@ -171,7 +171,10 @@ [:div#content_body body] [:div#footer "The content on 4clojure.com is available under the EPL v 1.0 license." - [:a#contact {:href "mailto:team@4clojure.com"} "Contact us!"]] + (let [email "team@4clojure.com"] + [:span + [:a#contact {:href (str "mailto:" email)} "Contact us"] + (str `(~email))])] (javascript-tag " var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-22844856-1']); From dac918c86fc103ba3c3435a7fd5a159c3a936f75 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Tue, 19 Jul 2011 14:09:01 -0700 Subject: [PATCH 13/13] Bump version --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 13152763..afe753b0 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject foreclojure "1.1.0" +(defproject foreclojure "1.1.1" :description "4clojure - a website for lisp beginners" :dependencies [[clojure "1.2.1"] [clojure-contrib "1.2.0"]