Permalink
Browse files

Stop storing times-solved for problems in mongo.

Instead, keep it in memory and update it when users
solve a new problem.
  • Loading branch information...
1 parent 57b9382 commit 16ebe34fa77401deb4e883ff2472e229297332aa @amalloy amalloy committed Sep 19, 2011
Showing with 72 additions and 58 deletions.
  1. +4 −9 src/foreclojure/graphs.clj
  2. +13 −11 src/foreclojure/mongo.clj
  3. +36 −36 src/foreclojure/problems.clj
  4. +2 −2 src/foreclojure/static.clj
  5. +17 −0 src/foreclojure/utils.clj
View
@@ -5,8 +5,7 @@
(:import [java.io ByteArrayInputStream
ByteArrayOutputStream])
(:use [compojure.core :only [defroutes GET]]
- [foreclojure.utils :only [from-mongo]]
- [somnium.congomongo :only [fetch-one]]
+ [foreclojure.problems :only [solved-stats]]
[useful.utils :only [with-adjustments]]))
(defn un-group
@@ -18,13 +17,9 @@
(repeat count x))))
(defn fetch-score-frequencies [problem-id]
- (into {}
- (for [[k v] (:scores
- (from-mongo
- (fetch-one :problems
- :where {:_id problem-id}
- :only [:scores])))]
- [(Integer/parseInt (name k)), v])))
+ (-> @solved-stats
+ (get problem-id)
+ (dissoc nil)))
(defn make-problem-plot [id best curr]
(let [freqs (fetch-score-frequencies id)
View
@@ -2,7 +2,7 @@
(:use somnium.congomongo
[foreclojure.data-set :only [load-problems]]
[foreclojure.config :only [config]]
- [foreclojure.problems :only [total-solved get-problem-list]]
+ [foreclojure.problems :only [number-from-mongo-key solved-stats get-problem-list]]
[foreclojure.users :only [get-users]]))
(defn connect-to-db []
@@ -43,16 +43,18 @@
(defn reconcile-solved-count
"Overwrites the times-solved field in the problems collection based on data from the users collection. Should only be called on server startup since it isn't a safe operation. Also updates the total-solved agent."
[]
- (let [total (->> (get-users)
- (mapcat :solved)
- (frequencies)
- (reduce (fn [sum [id solved]]
- (update! :problems
- {:_id id}
- {:$set {:times-solved solved}})
- (+ sum solved))
- 0))]
- (send total-solved (constantly total))))
+ (let [+ (fnil + 0)
+ [total scores]
+ (->> (fetch :users :only [:scores])
+ (mapcat :scores)
+ (frequencies)
+ (reduce (fn [[total scores] [[id score] times]]
+ [(+ total times)
+ (update-in scores
+ [(number-from-mongo-key id) score]
+ + times)])
+ [0 {}]))]
+ (send solved-stats (constantly (assoc scores :total total)))))
(defn prepare-mongo []
(connect-to-db)
@@ -4,7 +4,7 @@
[clojure.string :as s]
[ring.util.response :as response])
(:import [org.apache.commons.mail EmailException])
- (:use [foreclojure.utils :only [from-mongo get-user get-solved login-link *url* flash-msg flash-error def-page row-class approver? can-submit? send-email image-builder with-user]]
+ (:use [foreclojure.utils :only [from-mongo get-user get-solved login-link *url* flash-msg flash-error def-page row-class approver? can-submit? send-email image-builder with-user maybe-update]]
[foreclojure.social :only [tweet-link gist!]]
[foreclojure.feeds :only [create-feed]]
[foreclojure.users :only [golfer? get-user-id disable-codebox?]]
@@ -20,7 +20,7 @@
[compojure.core :only [defroutes GET POST]]
[clojure.contrib.json :only [json-str]]))
-(def total-solved (agent 0))
+(def solved-stats (agent {:total 0}))
(defn get-problem [x]
(from-mongo
@@ -31,7 +31,7 @@
([criteria]
(from-mongo
(fetch :problems
- :only [:_id :title :difficulty :tags :times-solved :user]
+ :only [:_id :title :difficulty :tags :user]
:where criteria
:sort {:_id 1}))))
@@ -80,6 +80,11 @@
[id]
(keyword (str (int id))))
+(defn number-from-mongo-key
+ "Turn a keyword like :4 into an integer"
+ [k]
+ (Integer. (name k)))
+
(defn trim-code [code]
(when code (.trim code)))
@@ -89,44 +94,39 @@
code)))
(defn record-golf-score! [user-id problem-id score]
- (let [user-score-key (keyword (str "scores." problem-id))
- problem-score-key (keyword (str "scores." score))
- [problem-scores-key user-subkey] (map mongo-key-from-number
- [score problem-id])]
- (when-let [{:keys [_id scores] :as user}
- (from-mongo
- (fetch-one :users
- :where {:_id user-id}))]
- (let [old-score-real (get scores user-subkey)
- old-score-test (or old-score-real 1e6)
- old-score-key (keyword (str "scores." old-score-real))]
- (when (golfer? user)
- (session/session-put! :golf-chart
- {:id problem-id
- :score score
- :best old-score-real}))
- (when (< score old-score-test)
- (update! :problems
- {:_id problem-id,
- old-score-key {:$gt 0}}
- {:$inc {old-score-key -1}})
- (update! :problems
- {:_id problem-id}
- {:$inc {problem-score-key 1}})
- (update! :users
- {:_id _id}
- {:$set {user-score-key score}}))))))
+ (when-let [{user-id :_id {old-score (keyword problem-id)} :scores :as user}
+ (from-mongo
+ (fetch-one :users
+ :where {:_id user-id}))]
+ (when (golfer? user)
+ (session/session-put! :golf-chart
+ {:id problem-id
+ :score score
+ :best old-score}))
+ (when (or (not old-score)
+ (> old-score score))
+ (update! :users
+ {:_id user-id}
+ {:$set {(keyword (str "scores." problem-id)) score}})
+ (send solved-stats (fn [scores]
+ (let [inc (fnil inc 0),
+ dec (fn [x]
+ (when (and x (> x 1))
+ (dec x)))]
+ (maybe-update scores [problem-id]
+ #(-> %
+ (maybe-update [score] inc)
+ (maybe-update [old-score] dec)))))))))
(defn store-completed-state! [username problem-id code]
(let [{user-id :_id} (fetch-one :users
:where {:user username}
:only [:_id])
current-time (java.util.Date.)]
(when (not-any? #{problem-id} (get-solved username))
- (update! :users {:_id user-id} {:$addToSet {:solved problem-id}})
- (update! :problems {:_id problem-id} {:$inc {:times-solved 1}})
- (update! :users {:_id problem-id} {:$set {:last-solved-date current-time}})
- (send total-solved inc))
+ (update! :users {:_id user-id} {:$addToSet {:solved problem-id}
+ :$set {:last-solved-date current-time}})
+ (send solved-stats update-in [:total] inc))
(record-golf-score! user-id problem-id (code-length code))
(save-solution user-id problem-id code)))
@@ -381,7 +381,7 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
(let [solved (get-solved (session/session-get :user))
problems (get-problem-list)]
(map-indexed
- (fn [x {:keys [title difficulty times-solved tags user], id :_id}]
+ (fn [x {:keys [title difficulty tags user], id :_id}]
[:tr (row-class x)
[:td.titlelink
[:a {:href (str "/problem/" id)}
@@ -391,7 +391,7 @@ Return a map, {:message, :error, :url, :num-tests-passed}."
(s/join " " (map #(str "<span class='tag'>" % "</span>")
tags))]
[:td.centered user]
- [:td.centered (int times-solved)]
+ [:td.centered (reduce + (vals (get @solved-stats id)))]
[:td.centered (checkbox-img (contains? solved id))]])
problems))])}))
@@ -1,6 +1,6 @@
(ns foreclojure.static
(:use [compojure.core :only [defroutes GET]]
- [foreclojure.problems :only [total-solved]]
+ [foreclojure.problems :only [solved-stats]]
[foreclojure.config :only [repo-url]]
[foreclojure.utils :only [def-page]]))
@@ -17,7 +17,7 @@
:content
[:div#welcome
[:div#totalcount
- (.format df (deref total-solved)) " problems solved and counting!"]
+ (.format df (:total @solved-stats)) " problems solved and counting!"]
[:div
[:h3 "What is 4Clojure?"]
[:p "4Clojure is a resource to help fledgling clojurians learn the language through interactive problems. The first few problems are easy enough that even someone with no prior experience should find the learning curve forgiving. See 'Help' for more information."]]
View
@@ -36,6 +36,23 @@
~fail-expr
~body))
+
+
+(defn maybe-update
+ "Acts like clojure.core/update-in, except that if the value being assoc'd in
+ is nil, then instead the key is dissoc'd entirely."
+ ([m ks f]
+ (let [[k & ks] ks
+ inner (get m k)
+ v (if ks
+ (maybe-update inner ks f)
+ (f inner))]
+ (if v
+ (assoc m k v)
+ (dissoc m k))))
+ ([m ks f & args]
+ (maybe-update m ks #(apply f % args))))
+
(defn image-builder
"Return a function for constructing an [:img] element from a keyword.

0 comments on commit 16ebe34

Please sign in to comment.