Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release/1.3.0'

  • Loading branch information...
commit 7abcce88ad7ddcf0f24eeb33957981452d79d938 2 parents 667590b + 86d5685
@amalloy amalloy authored
View
10 README.md
@@ -3,13 +3,14 @@
An interactive problem website for Clojure beginners
[https://www.4clojure.com](https://www.4clojure.com).
-## State of the Art
+## Contributing
-This site is in a very early stage of development, so there is not a
-lot of polish yet. Anyone interested in contributing should check out
+Anyone interested in contributing should check out
the [Issues](https://github.com/dbyrne/4clojure/issues) page for ideas
on what to work on.
+Join us in #4clojure on freenode for help or discussion.
+
## Setup instructions for running locally
* Download and install [leiningen](https://github.com/technomancy/leiningen).
@@ -32,9 +33,8 @@ vaguely like this:
* For the first time use, you will need to load the problem data. Run the script `load-data.sh`:
./load-data.sh
-* Run `lein run` and then open the brower to http://localhost:8080/
- lein run
+* Run `lein ring server`
## Contributors
View
7 project.clj
@@ -1,4 +1,4 @@
-(defproject foreclojure "1.2.0"
+(defproject foreclojure "1.3.0"
:description "4clojure - a website for lisp beginners"
:dependencies [[clojure "1.2.1"]
[clojure-contrib "1.2.0"]
@@ -16,8 +16,9 @@
[incanter/incanter-core "1.2.3"]
[incanter/incanter-charts "1.2.3"]
[org.apache.commons/commons-email "1.2"]]
- :dev-dependencies [[lein-ring "0.4.0"]
+ :dev-dependencies [[lein-ring "0.4.5"]
[swank-clojure "1.2.1"]
[midje "1.1.1"]]
:main foreclojure.core
- :ring {:handler foreclojure.core/app})
+ :ring {:handler foreclojure.core/app
+ :init foreclojure.mongo/prepare-mongo})
View
27 resources/public/css/style.css
@@ -116,6 +116,18 @@ a.novisited {color: #00e;}
#menu a {text-decoration: none; font-size: 14px; font-weight:bold;}
#menu a.menu {margin: 15px;}
+.user-follow-button{
+ margin-left: 20px;
+ margin-top: -3px;
+ margin-bottom: 10px;
+}
+
+.user-profile-name{
+ float: left;
+ font-weight: bold;
+ font-size: 15px;
+}
+
.user-profile-link{
text-decoration: none;
color: black;
@@ -210,10 +222,16 @@ div.message {
float: left;
}
-span#prob-title {
- font-size: 20px;
+div#prob-title {
+ font-size: 15pt;
font-family: sans-serif;
font-weight: bold;
+ float: left;
+}
+
+button#solutions-link {
+ margin-left: 20px;
+ margin-top: -1px;
}
div#prob-desc {
@@ -436,6 +454,11 @@ a.graph-class :hover {color: #445599;}
width: 90%;
}
+.ranking {
+ font-weight: bold;
+ font-size: 14px;
+}
+
#github-banner {
float: right;
display: block;
View
5 src/foreclojure/core.clj
@@ -8,7 +8,7 @@
[foreclojure.login :only [login-routes]]
[foreclojure.register :only [register-routes]]
[foreclojure.golf :only [golf-routes]]
- [foreclojure.ring :only [resources wrap-strip-trailing-slash wrap-url-as-file]]
+ [foreclojure.ring :only [resources wrap-strip-trailing-slash wrap-url-as-file wrap-versioned-expiry]]
[foreclojure.users :only [users-routes]]
[foreclojure.config :only [config]]
[foreclojure.social :only [social-routes]]
@@ -37,7 +37,8 @@
golf-routes
(-> (resources "/*")
(wrap-url-as-file)
- (wrap-file-info))
+ (wrap-file-info)
+ (wrap-versioned-expiry))
(route/not-found "Page not found"))
(def app (-> #'main-routes
View
12 src/foreclojure/git.clj
@@ -0,0 +1,12 @@
+(ns foreclojure.git
+ (:require [clojure.string :as s])
+ (:use [clojure.java.shell :only [sh]]))
+
+(letfn [(cmd [& args]
+ (not-empty (s/trim (:out (apply sh args)))))]
+
+ ;; fetch these at load time rather than on demand, so that it's accurate even
+ ;; if someone checks out a different revision to poke at without restarting
+ ;; the server (eg to diagnose bugs in a release)
+ (def sha (cmd "git" "rev-parse" "--verify" "HEAD"))
+ (def tag (cmd "git" "describe" "--abbrev=0" "master")))
View
40 src/foreclojure/login.clj
@@ -4,13 +4,13 @@
(:import [org.jasypt.util.password StrongPasswordEncryptor])
(:use [hiccup.form-helpers :only [form-to label text-field password-field check-box]]
[foreclojure.utils :only [def-page from-mongo flash-error flash-msg with-user form-row assuming send-email login-url]]
- [foreclojure.users :only [disable-codebox? set-disable-codebox]]
+ [foreclojure.users :only [disable-codebox? set-disable-codebox hide-solutions? set-hide-solutions]]
[compojure.core :only [defroutes GET POST]]
[useful.map :only [keyed]]
[clojail.core :only [thunk-timeout]]
[clojure.stacktrace :only [print-cause-trace]]
[somnium.congomongo :only [update! fetch-one]]))
-
+
(def-page my-login-page [location]
{:title "4clojure - login"
:content
@@ -50,6 +50,7 @@
(response/redirect (or location "/problems")))
(flash-error "Error logging in." "/login"))))
+;; TODO this page is getting hella gross. Need a real Settings page soon.
(def-page update-credentials-page []
{:title "Change password"
:content
@@ -66,17 +67,30 @@
[password-field :pwd "New password"]
[password-field :repeat-pwd "Repeat password"]])
[:tr
- [:td [:button {:type "submit"} "Reset now"]]])]
- [:div#settings-codebox
- [:h2 "Disable JavaScript Code Box"]
- [:p "Selecting this will disable the JavaScript code entry box and just give you plain text entry"]
- (form-to [:post "/users/set-disable-codebox"]
- (check-box :disable-codebox
+ [:td [:button {:type "submit"} "Reset now"]]])]
+ [:hr]
+ [:div#settings-codebox
+ [:h2 "Disable JavaScript Code Box"]
+ [:p "Selecting this will disable the JavaScript code entry box and just give you plain text entry"]
+ (form-to [:post "/users/set-disable-codebox"]
+ (check-box :disable-codebox
(disable-codebox? user-obj))
- [:label {:for "disable-codebox"}
+ [:label {:for "disable-codebox"}
"Disable JavaScript in code entry box"]
[:br]
[:div#button-div
+ [:button {:type "submit"} "Submit"]])]
+ [:hr]
+ [:div#settings-follow
+ [:h2 "Hide My Solutions"]
+ [:p "When you solve a problem, we allow any user who has solved a problem to view your solutions to that problem. Check this box to keep your solutions private."]
+ (form-to [:post "/users/set-hide-solutions"]
+ (check-box :hide-solutions
+ (hide-solutions? user-obj))
+ [:label {:for "hide-solutions"}
+ "Hide my solutions"]
+ [:br]
+ [:div#button-div
[:button {:type "submit"} "Submit"]])]]])})
(defn do-update-credentials! [new-username old-pwd new-pwd repeat-pwd]
@@ -181,10 +195,14 @@
(GET "/login/reset" [] (reset-password-page))
(POST "/login/reset" [email]
(do-reset-password! email))
-
+
(GET "/logout" []
(do (session/session-delete-key! :user)
(response/redirect "/")))
(POST "/users/set-disable-codebox" [disable-codebox]
- (set-disable-codebox disable-codebox)))
+ (set-disable-codebox disable-codebox))
+
+ (POST "/users/set-hide-solutions" [hide-solutions]
+ (println "POST")
+ (set-hide-solutions hide-solutions)))
View
94 src/foreclojure/problems.clj
@@ -55,7 +55,7 @@
([problem]
(str "Now try " (problem-link problem) "!"))
([skipped not-tried]
- (str "Now move on to " (problem-link not-tried)
+ (str "Now you can move on to " (problem-link not-tried)
", or go back and try " (problem-link skipped) " again!"))))
(defn next-problem-link [completed-problem-id]
@@ -84,7 +84,8 @@
(when code (.trim code)))
(defn code-length [code]
- (count (remove #(Character/isWhitespace %)
+ (count (remove #(or (Character/isWhitespace %)
+ (= % \,))
code)))
(defn record-golf-score! [user-id problem-id score]
@@ -132,20 +133,24 @@
(defn mark-completed [problem code & [user]]
(let [user (or user (session/session-get :user))
{:keys [_id approved]} problem
- gist-link (html [:div.share
- [:a.novisited {:href "/share/code"} "Share"]
- " this solution with your friends!"])
+ gist-link (html [:span.share
+ [:a.novisited {:href "/share/code"} "share"]
+ " this solution on github and twitter! "])
message
(cond
(not approved) (str "You've solved the unapproved problem. Now you can approve it!")
user (do
(store-completed-state! user _id code)
- (str "Congratulations, you've solved the problem!"
- "<br />" (next-problem-link _id)))
- :else (str "You've solved the problem! If you "
- (login-link "log in" (str "/problem/" _id)) " we can track your progress."))]
+ (str "Congratulations, you've solved the problem! See the "
+ "<a href='/problem/solutions/" _id "'>solutions</a>"
+ " that the users you follow have submitted, or "
+ gist-link
+ (next-problem-link _id)))
+ :else (str "You've solved the problem; "
+ gist-link
+ "You need to " (login-link "log in" (str "/problem/" _id)) " in order to save your solutions and track progress."))]
(session/session-put! :code [_id code])
- {:message (str message " " gist-link), :error "", :url (str "/problem/" _id)}))
+ {:message message, :error "", :url (str "/problem/" _id)}))
(def restricted-list '[use require in-ns future agent send send-off pmap pcalls])
@@ -253,7 +258,7 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
(let [{:keys [_id title difficulty tags description
restricted tests approved user]}
(get-problem (Integer. id)),
-
+ session-user (session/session-get :user)
title (str (when-not approved
"Unapproved: ")
title)]
@@ -261,7 +266,14 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
{:title (str _id ". " title)
:content
[:div
- [:span#prob-title title]
+ [:div#prob-title title]
+ (if session-user
+ (with-user [{:keys [solved]}]
+ (if (some #{(Integer. id)} solved)
+ (link-to (str "/problem/solutions/" id)
+ [:button#solutions-link {:type "submit"} "Solutions"])
+ [:div {:style "clear: right; margin-bottom: 15px;"} "&nbsp;"]))
+ [:div {:style "clear: right; margin-bottom: 15px;"} "&nbsp;"])
[:hr]
[:table#tags
[:tr [:td "Difficulty:"] [:td (or difficulty "N/A")]]
@@ -311,6 +323,43 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
(code-box id)
(flash-error "You cannot access this page" "/problems")))
+(def-page show-solutions-page [problem-id]
+ {:title "4Clojure - Problem Solutions"
+ :content
+ (list
+ [:div.message (session/flash-get :message)]
+ [:div#problems-error.error (session/flash-get :error)]
+ [:h3 {:style "margin-top: -20px;"} "Solutions:"]
+ (with-user [{:keys [following]}]
+ (if (empty? following)
+ [:p "You can only see solutions of users whom you follow. Click on any name from the " (link-to "/users" "users") " listing page to see their profile, and click follow from there."]
+ (if (some (complement nil?) (map #(get-solution :public % problem-id) following))
+ (interpose [:hr {:style "margin-top: 20px; margin-bottom: 20px;"}]
+ (for [f-user-id following
+ :let [f-user (:user (from-mongo
+ (fetch-one :users
+ :where {:_id f-user-id}
+ :only [:user])))
+ f-code (get-solution :public
+ f-user-id problem-id)]
+ :when f-code]
+ [:div.follower-solution
+ [:div.follower-username (str f-user "'s solution:")]
+ [:pre.follower-code f-code]]))
+ [:p "None of the users you follow have solved this problem yet!"]))))})
+
+(defn show-solutions [id]
+ (let [problem-id (Integer. id)
+ user (session/session-get :user)]
+ (if user
+ (with-user [{:keys [solved]}]
+ (if (some #{problem-id} solved)
+ (show-solutions-page problem-id)
+ (flash-error "You must solve this problem before you can see others' solutions!" (str "/problem/" problem-id))))
+ (do
+ (session/session-put! :login-to (str "/problem/solutions/" problem-id))
+ (flash-error "You must login to see solutions!" "/login")))))
+
(let [checkbox-img (image-builder {true ["/images/checkmark.png" "completed"]
false ["/images/empty-sq.png" "incomplete"]})]
(def-page problem-list-page []
@@ -409,16 +458,21 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
"create a user submitted problem"
[title difficulty tags restricted description code id author]
(let [user (session/session-get :user)]
- (if (can-submit? user)
+ (if (or (approver? user)
+ (and (can-submit? user)
+ (not id)))
(let [id (or id
(:seq (fetch-and-modify
:seqs
{:_id "problems"}
{:$inc {:seq 1}})))
edit-url (str "https://4clojure.com/problem/"
- id)]
+ id)
+ approved (true? (:approved (fetch-one :problems
+ :where {:_id id}
+ :only [:approved])))]
- (when (empty? author) ; newly submitted, not a moderator tweak
+ (when (empty? author) ; newly submitted, not a moderator tweak
(try
(send-email
{:from "team@4clojure.com"
@@ -442,7 +496,7 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
:restricted (re-seq #"\S+" restricted)
:tests (s/split code #"\r\n\r\n")
:user (if (empty? author) user author)
- :approved false})
+ :approved approved})
(flash-msg "Thank you for submitting a problem! Be sure to check back to see it posted." "/problems"))
(flash-error "You are not authorized to submit a problem." "/problems"))))
@@ -501,17 +555,21 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
(POST "/problems/submit" [prob-id author title difficulty tags restricted description code]
(create-problem title difficulty tags restricted description code (when (not= "" prob-id) (Integer. prob-id)) author))
(GET "/problems/unapproved" [] (unapproved-problem-list))
+ (GET "/problem/:id/edit" [id]
+ (edit-problem (Integer. id)))
(POST "/problem/edit" [id]
(edit-problem (Integer. id)))
(POST "/problem/approve" [id]
(approve-problem (Integer. id)))
(POST "/problem/reject" [id]
(reject-problem (Integer. id) "We didn't like your problem."))
+ (GET "/problem/solutions/:id" [id]
+ (show-solutions id))
(POST "/problem/:id" [id code]
(static-run-code (Integer. id) (trim-code code)))
(POST "/rest/problem/:id" [id code]
- {:headers {"Content-Type" "application/json"}}
- (rest-run-code (Integer. id) (trim-code code)))
+ {:headers {"Content-Type" "application/json"}}
+ (rest-run-code (Integer. id) (trim-code code)))
(GET "/problems/rss" [] (create-feed
"4Clojure: Recent Problems"
"http://4clojure.com/problems"
View
9 src/foreclojure/ring.clj
@@ -3,6 +3,7 @@
[clojure.string :as s])
(:import [java.net URL])
(:use [compojure.core :only [GET]]
+ [foreclojure.utils :only [strip-version-number]]
[ring.util.response :only [response]]))
;; copied from compojure.route, modified to use File instead of Stream
@@ -27,3 +28,11 @@
(defn wrap-strip-trailing-slash [handler]
(fn [request]
(handler (update-in request [:uri] s/replace #"(?<=.)/$" ""))))
+
+(defn wrap-versioned-expiry [handler]
+ (fn [request]
+ (-> request
+ (update-in [:uri] strip-version-number)
+ (handler)
+ (assoc-in [:headers "Cache-control"]
+ "public, max-age=31536000"))))
View
15 src/foreclojure/solutions.clj
@@ -3,10 +3,17 @@
(:use [somnium.congomongo :only [fetch-one update!]]
[useful.debug :only [?]]))
-(defn get-solution [user-id problem-id]
- (:code (fetch-one :solutions
- :where {:user user-id
- :problem problem-id})))
+(defn get-solution
+ ([perm-level user-id problem-id]
+ (when (or (= :private perm-level)
+ (not (:hide-solutions (fetch-one :users
+ :where {:_id user-id}
+ :only [:hide-solutions]))))
+ (get-solution user-id problem-id)))
+ ([user-id problem-id]
+ (:code (fetch-one :solutions
+ :where {:user user-id
+ :problem problem-id}))))
(defn save-solution [user-id problem-id code]
(update! :solutions
View
3  src/foreclojure/static.clj
@@ -31,7 +31,8 @@
[:h3 "So wait, I can't buy cheap real estate here?"]
[:p "At this time, 4clojure.com does not provide information regarding the sale of foreclosed homes, and has no plans of doing so in the future."]]
[:img {:src "/images/PoweredMongoDBbeige50.png"
- :alt "Powered by MongoDB"}]]})
+ :alt "Powered by MongoDB"
+ :width 129 :height 61}]]})
(def-page help-page []
{:title "Help"
View
121 src/foreclojure/users.clj
@@ -1,9 +1,11 @@
(ns foreclojure.users
- (:require [ring.util.response :as response])
+ (:require [ring.util.response :as response]
+ [sandbar.stateful-session :as session])
(:use [foreclojure.utils :only [from-mongo def-page row-class get-user with-user]]
[foreclojure.config :only [config repo-url]]
[somnium.congomongo :only [fetch-one fetch update!]]
- [compojure.core :only [defroutes GET]]
+ [compojure.core :only [defroutes GET POST]]
+ [hiccup.form-helpers :only [form-to hidden-field]]
[hiccup.page-helpers :only [link-to]]))
(def golfer-tags (into [:contributor]
@@ -28,12 +30,33 @@
sortfn (comp - count :solved)]
(sort-by sortfn users)))
+(defn get-user-with-ranking [username, users]
+ (when username
+ (let [total (count users)
+ users-with-rankings (map-indexed
+ (fn [idx itm]
+ (assoc itm :rank
+ (str (inc idx) " out of " total)))
+ users)]
+ (first
+ (filter #(= username (% :user)) users-with-rankings)))))
+
+(defn get-top-100-and-current-user [username]
+ (let [users (get-users)
+ user-ranking (get-user-with-ranking username users)]
+ {:user-ranking user-ranking
+ :top-100 (take 100 users)}))
+
+
(defn golfer? [user]
(some user golfer-tags))
(defn disable-codebox? [user]
(true? (:disable-code-box user)))
+(defn hide-solutions? [user]
+ (true? (:hide-solutions user)))
+
(defn email-address [username]
(:email (fetch-one :users :where {:user username})))
@@ -41,28 +64,46 @@
(link-to (str "mailto:" (email-address username))
username))
-(def-page users-page []
- {:title "Top Users"
- :content
- (list
+(defn format-user-ranking [{:keys [rank user contributor solved]}]
+ (when user
[:div
- [:span.contributor "*"] " "
- (link-to repo-url "4clojure contributor")]
+ [:h2 "Your Ranking"]
+ [:div.ranking (str "Username: ")
+ (when contributor [:span.contributor "* "])
+ [:a.user-profile-link {:href (str "/user/" user)} user]]
+ [:div.ranking (str "Rank: " rank)]
+ [:div.ranking (str "Problems Solved: " (count solved))]
[:br]
- [:table#user-table.my-table
- [:thead
- [:tr
- [:th {:style "width: 40px;"} "Rank"]
- [:th "Username"]
- [:th "Problems Solved"]]]
- (map-indexed (fn [rownum {:keys [user contributor solved]}]
- [:tr (row-class rownum)
- [:td (inc rownum)]
- [:td
- (when contributor [:span.contributor "* "])
- [:a.user-profile-link {:href (str "/user/" user)} user]]
- [:td.centered (count solved)]])
- (get-users))])})
+ [:br]]))
+
+
+(def-page users-page []
+ (let [username (session/session-get :user)
+ {:keys [user-ranking top-100]} (get-top-100-and-current-user username)]
+ {:title "Top 100 Users"
+ :content
+ (list
+ [:h1 "Top 100 Users"]
+ (format-user-ranking user-ranking)
+ [:div
+ [:span.contributor "*"] " "
+ (link-to repo-url "4clojure contributor")]
+ [:br]
+ [:table#user-table.my-table
+ [:thead
+ [:tr
+ [:th {:style "width: 40px;"} "Rank"]
+ [:th "Username"]
+ [:th "Problems Solved"]]]
+ (map-indexed (fn [rownum {:keys [user contributor solved]}]
+ [:tr (row-class rownum)
+ [:td (inc rownum)]
+ [:td
+ (when contributor [:span.contributor "* "])
+ [:a.user-profile-link {:href (str "/user/" user)} user]]
+ [:td.centered (count solved)]])
+ top-100)])}))
+
;; TODO: this is snagged from problems.clj but can't be imported due to cyclic dependency, must refactor this out.
(defn get-problems
@@ -88,11 +129,22 @@
(filter ids (get-solved username)))))
(def-page user-profile [username]
- (let [page-title (str "User: " username)]
+ (let [page-title (str "User: " username)
+ user-id (:_id (get-user username))]
{:title page-title
:content
(list
- [:h2 page-title]
+ [:div.user-profile-name page-title]
+ (if (session/session-get :user)
+ (with-user [{:keys [_id following]}]
+ (if (not= _id user-id)
+ (let [[url label] (if (some #{user-id} following)
+ ["unfollow" "Unfollow"]
+ ["follow" "Follow"])]
+ (form-to [:post (str "/user/" url "/" username)]
+ [:button.user-follow-button {:type "submit"} label]))
+ [:div {:style "clear: right; margin-bottom: 10px;"} "&nbsp;"]))
+ [:div {:style "clear: right; margin-bottom: 10px;"} "&nbsp;"])
[:hr]
[:table
(for [difficulty ["Elementary" "Easy" "Medium" "Hard"]]
@@ -112,6 +164,14 @@
(count (get-solved username)) "/"
(count (get-problems))]]])}))
+(defn follow-user [username operation]
+ (with-user [{:keys [_id]}]
+ (let [follow-id (:_id (get-user username))]
+ (update! :users
+ {:_id _id}
+ {operation {:following follow-id}})))
+ (response/redirect (str "/user/" username)))
+
(defn set-disable-codebox [disable-flag]
(with-user [{:keys [_id]}]
(update! :users
@@ -119,6 +179,15 @@
{:$set {:disable-code-box (boolean disable-flag)}})
(response/redirect "/problems")))
+(defn set-hide-solutions [hide-flag]
+ (with-user [{:keys [_id]}]
+ (update! :users
+ {:_id _id}
+ {:$set {:hide-solutions (boolean hide-flag)}})
+ (response/redirect "/problems")))
+
(defroutes users-routes
- (GET "/users" [] (users-page))
- (GET "/user/:username" [username] (user-profile username)))
+ (GET "/users" [] (users-page))
+ (GET "/user/:username" [username] (user-profile username))
+ (POST "/user/follow/:username" [username] (follow-user username :$addToSet))
+ (POST "/user/unfollow/:username" [username] (follow-user username :$pull)))
View
69 src/foreclojure/utils.clj
@@ -1,15 +1,18 @@
(ns foreclojure.utils
(:require [sandbar.stateful-session :as session]
[ring.util.response :as response]
- [clojure.walk :as walk])
+ [clojure.walk :as walk]
+ [clojure.string :as string]
+ [foreclojure.git :as git]
+ [hiccup.page-helpers :as hiccup])
(:import [java.net URLEncoder]
[org.apache.commons.mail HtmlEmail])
(:use [hiccup.core :only [html]]
- [hiccup.page-helpers :only [doctype include-css javascript-tag link-to include-js]]
+ [hiccup.page-helpers :only [doctype javascript-tag link-to]]
[hiccup.form-helpers :only [label]]
[useful.fn :only [to-fix]]
[somnium.congomongo :only [fetch-one]]
- [foreclojure.config :only [config]]))
+ [foreclojure.config :only [config repo-url]]))
(def ^{:dynamic true} *url* nil)
@@ -18,9 +21,6 @@
(binding [*url* (:uri req)]
(handler req))))
-(defmacro dbg [x]
- `(let [x# ~x] (println '~x "=" x#) x#))
-
(defmacro assuming
"Guard body with a series of tests. Each clause is a test-expression
followed by a failure value. Tests will be performed in order; if
@@ -36,8 +36,17 @@
~fail-expr
~body))
-(defn image-builder [data & {:keys [alt src] :or {alt identity,
- src identity}}]
+(defn image-builder
+ "Return a function for constructing an [:img] element from a keyword.
+
+ data should be a map from image \"names\" to pairs [src, alt]. The function
+ returned by image-builder will look up its argument as an image name, and
+ return an img element with the appropriate src and alt attributes.
+
+ Optionally, additional keyword arguments :alt and :src may be supplied to
+ image-builder - these functions will be called to transform the alt and src
+ attributes of the returned img."
+ [data & {:keys [alt src] :or {alt identity, src identity}}]
(fn [key]
(let [[src-prop alt-prop] (get data key)]
[:img {:src (src src-prop)
@@ -108,22 +117,29 @@
(def approver? (user-attribute :approver))
(defn can-submit? [username]
- (and (:problem-submission config)
- (>= (count (get-solved username))
- (:advanced-user-count config))))
+ (or (approver? username)
+ (and (:problem-submission config)
+ (>= (count (get-solved username))
+ (:advanced-user-count config)))))
+
(defprotocol PageWriter
+ "Specify how an object should be converted to the {:title \"foo\" :content
+ [:div ...] :baz-attr true} format used by def-page for rendering pages."
(page-attributes [this]))
(extend-protocol PageWriter
clojure.lang.IPersistentMap
+ ;; Supplied map should be used verbatim
(page-attributes [this] this)
Object
+ ;; User probably just returned a Hiccup structure; shove it into :content
(page-attributes [this]
{:content this})
nil
+ ;; Allow to return nothing at all so Compojure keeps looking
(page-attributes [this] nil))
(let [defaults {:content nil
@@ -132,6 +148,21 @@
(defn rendering-info [attributes]
(into defaults attributes)))
+(let [version-suffix (str "__" git/tag)]
+ (defn add-version-number [file]
+ (let [[_ path ext] (re-find #"(.*)\.(.*)$" file)]
+ (str path version-suffix "." ext)))
+
+ (defn strip-version-number [file]
+ (string/replace file version-suffix "")))
+
+(letfn [(wrap-versioning [f]
+ (fn [& files]
+ (for [file files]
+ (f (add-version-number file)))))]
+ (def js (wrap-versioning hiccup/include-js))
+ (def css (wrap-versioning hiccup/include-css)))
+
(defn html-doc [body]
(let [attrs (rendering-info (page-attributes body))
user (session/session-get :user)]
@@ -142,19 +173,21 @@
[:title (:title attrs)]
[:link {:rel "alternate" :type "application/atom+xml" :title "Atom" :href "http://4clojure.com/problems/rss"}]
[:link {:rel "shortcut icon" :href "/favicon.ico"}]
- (include-js "/vendor/script/jquery-1.5.2.min.js" "/vendor/script/jquery.dataTables.min.js")
- (include-js "/script/foreclojure.js")
- (include-js "/vendor/script/xregexp.js" "/vendor/script/shCore.js" "/vendor/script/shBrushClojure.js")
- (include-js "/vendor/script/ace/ace.js" "/vendor/script/ace/mode-clojure.js")
- (include-css "/css/style.css" "/css/demo_table.css" "/css/shCore.css" "/css/shThemeDefault.css")
[:style {:type "text/css"}
".syntaxhighlighter { overflow-y: hidden !important; }"]
+ (css "/css/style.css" "/css/demo_table.css" "/css/shCore.css" "/css/shThemeDefault.css")
+ (js "/vendor/script/jquery-1.5.2.min.js" "/vendor/script/jquery.dataTables.min.js")
+ (js "/script/foreclojure.js")
+ (js "/vendor/script/xregexp.js" "/vendor/script/shCore.js" "/vendor/script/shBrushClojure.js")
+ (js "/vendor/script/ace/ace.js" "/vendor/script/ace/mode-clojure.js")
[:script {:type "text/javascript"} "SyntaxHighlighter.all()"]]
[:body
(when (:fork-banner attrs)
- [:div#github-banner [:a {:href "http://github.com/4clojure" :alt "Fork 4Clojure on Github!"}]])
+ [:div#github-banner [:a {:href repo-url
+ :alt "Fork 4Clojure on Github!"}]])
[:div#top
- (link-to "/" [:img#logo {:src "/images/logo.png" :alt "4clojure.com"}])]
+ (link-to "/" [:img#logo {:src "/images/logo.png" :alt "4clojure.com"
+ :width 230 :height 57}])]
[:div#content
[:br]
[:div#menu
View
11 src/foreclojure/version.clj
@@ -1,20 +1,15 @@
(ns foreclojure.version
(:use [foreclojure.utils :only [def-page]]
[foreclojure.config :only [repo-url]]
- [compojure.core :only [defroutes GET]]
- [clojure.java.shell :only [sh]]))
-
-;; fetch this at load time rather than on demand, so that it's accurate even
-;; if someone checks out a different revision to poke at without restarting
-;; the server (eg to diagnose bugs in a release)
-(def sha (not-empty (:out (sh "git" "rev-parse" "--verify" "HEAD"))))
+ [foreclojure.git :only [sha]]
+ [compojure.core :only [defroutes GET]]))
(def-page version []
{:title "About/version"
:content
(if sha
[:p "SHA: "
- [:a {:href (str repo-url "/commit/" sha)} sha]]
+ [:a {:href (str repo-url "/tree/" sha)} sha]]
[:p "No git repository found"])})
(defroutes version-routes
View
15 test/foreclojure/test/users.clj
@@ -9,6 +9,7 @@
[{:user "user1" :solved [1] } {:user "user2" :solved [1 2 3 4]}
{:user "user3" :solved [2 2] } {:user "user4" :solved [3]}])
(def users-sorted-by-solved (users-sort users-by-solved))
+
(fact
(:user (first users-sorted-by-solved)) => "user2")
(fact
@@ -23,8 +24,22 @@
{:user "user3" } {:user "user4" :last-login date1}])
(sort-by :last-login users-by-date)
(def users-sorted-by-date (users-sort users-by-date))
+
(fact
(:user (first users-sorted-by-date)) => "user2")
(fact
(:user (last users-sorted-by-date)) => "user3"))
+(deftest test-user-with-ranking
+ (def users [{:user "user1", :solved [1 2 3 4]}
+ {:user "user2", :solved [1 2 3]}
+ {:user "user3", :sovled [1 2]}
+ {:user "user4", :solved [1]}])
+
+
+ (facts "about user"
+ (:rank (get-user-with-ranking "user1" users)) => "1 out of 4"
+ (:rank (get-user-with-ranking "user2" users)) => "2 out of 4"
+ (:rank (get-user-with-ranking "user3" users)) => "3 out of 4"
+ (:rank (get-user-with-ranking "user4" users)) => "4 out of 4" ))
+
Please sign in to comment.
Something went wrong with that request. Please try again.