brian0.1.3a web app for data heavy personal websites dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||||||||
The AppThe application can be split into the following verticals:
| (ns brian.handler
(:require [compojure.core :refer [defroutes routes]]
[ring.middleware.resource :refer [wrap-resource]]
[ring.middleware.file-info :refer [wrap-file-info]]
[ring.middleware.json :refer [wrap-json-response]]
[hiccup.middleware :refer [wrap-base-url]]
[compojure.handler :as handler]
[compojure.route :as route]
[brian.routes.api :refer [api-routes]]
[brian.routes.home :refer [home-routes]])) | ||||||||||||||||||||||||
(defn init [] (println "brian is starting")) | |||||||||||||||||||||||||
(defn destroy [] (println "brian is shutting down")) | |||||||||||||||||||||||||
(defroutes app-routes (route/resources "/") (route/not-found "Not Found")) | |||||||||||||||||||||||||
(def app
(-> (routes home-routes api-routes app-routes)
(handler/site)
(wrap-base-url)
(wrap-json-response))) | |||||||||||||||||||||||||
The DataInitialize, load and save the in-memory database. Why datalog?
| (ns brian.models.db
(:require [clojure-csv.core :as csv]
[clojure.string :refer [blank?]]
[clojure.set :refer [rename-keys]]
[fogus.datalog.bacwn :refer
[q build-work-plan run-work-plan]]
[fogus.datalog.bacwn.macros :refer [<- ?- make-database]]
[fogus.datalog.bacwn.impl.rules :refer [rules-set]]
[fogus.datalog.bacwn.impl.database :refer [add-tuples]])) | ||||||||||||||||||||||||
(def db-base
(make-database
(relation :book [:isbn :author :title])
(index :book :isbn)
(relation :start [:isbn :when])
(index :start :isbn)
(relation :finish [:isbn :when])
(index :finish :isbn))) | |||||||||||||||||||||||||
(def rules
(rules-set
(<- (:attempt :author ?a :title ?t :when ?w)
(:book :isbn ?id :author ?a :title ?t)
(:start :isbn ?id :when ?w))
(<- (:success :author ?a :title ?t :when ?w)
(:book :isbn ?id :author ?a :title ?t)
(:finish :isbn ?id :when ?w)))) | |||||||||||||||||||||||||
(def wp-1 (build-work-plan rules (?- :attempt :author ?a
:title ?t :when ?q))) | |||||||||||||||||||||||||
Given spreadsheet data, construct a vector of maps using the first row as keys. | (defn get-tabular-data
[path delim]
(let [tsv (csv/parse-csv (slurp path) :delimiter delim)
hdrs (first tsv)
rows (rest tsv)]
(mapv #(zipmap hdrs %) rows))) | ||||||||||||||||||||||||
Filter map by keys ( | (defn filter-and-swap
[d old-ks new-ks]
(rename-keys (select-keys d old-ks)
(zipmap old-ks new-ks))) | ||||||||||||||||||||||||
(defn parse-entries [relation d old-ks new-ks]
"TODO iterate through the entries once
TODO use a map instead of the two vectors"
(let [massaged (mapv #(filter-and-swap % old-ks new-ks) d)
filtered (filter #(not-any? blank? (vals %)) massaged)]
(mapv #(flatten (cons relation %)) filtered))) | |||||||||||||||||||||||||
(defn parse-books [d]
(parse-entries :book d
["Title" "Author" "ISBN"]
[:title :author :isbn]))
(defn parse-starts [d]
(parse-entries :start d
["ISBN" "DateAdded"]
[:isbn :when]))
(defn parse-finishes [d]
(parse-entries :finish d
["ISBN" "DateRead"]
[:isbn :when])) | |||||||||||||||||||||||||
(def db
(let [raw (get-tabular-data "resources/shelfari_data.tsv" \tab)
parsed (concat (parse-books raw)
(parse-starts raw)
(parse-finishes raw))]
(apply (partial add-tuples db-base) parsed))) | |||||||||||||||||||||||||
Query the database for books that have been read. | (defn books-read
[]
(q (?- :attempt :author ?a :title ?t :when ?w)
db rules {})) | ||||||||||||||||||||||||
(ns brian.repl
(:use brian.handler
ring.server.standalone
[ring.middleware file-info file])) | |||||||||||||||||||||||||
(defonce server (atom nil)) | |||||||||||||||||||||||||
| (defn get-handler
[]
(-> #'app
; Makes static assets in $PROJECT_DIR/resources/public/ available.
(wrap-file "resources")
; Content-Type, Content-Length, and Last Modified headers
; for files in body
(wrap-file-info))) | ||||||||||||||||||||||||
Start the server in development mode from the REPL. | (defn start-server
[& [port]]
(let [port (if port (Integer/parseInt port) 8080)]
(reset! server
(serve (get-handler)
{:port port
:init init
:auto-reload? true
:destroy destroy
:join true}))
(println (str "You can view the site at http://localhost:" port)))) | ||||||||||||||||||||||||
Stop the server from the REPL. | (defn stop-server [] (.stop @server) (reset! server nil)) | ||||||||||||||||||||||||
The API | (ns brian.routes.api
(:require [compojure.core :refer :all]
[clj-time.core :as t]
[clj-time.format :as f]
[brian.models.db :refer [db rules]]
[fogus.datalog.bacwn :refer [q]]
[fogus.datalog.bacwn.macros :refer [?-]])) | ||||||||||||||||||||||||
(defn parse-year ([x] (parse-year "MM/dd/yyyy" x)) ([fmt x] (t/year (f/parse (f/formatter fmt) x)))) | |||||||||||||||||||||||||
(defn books-read []
(q (?- :success :author ?a :title ?t :when ?w) db rules {})) | |||||||||||||||||||||||||
Partition books by year read. | (defn books-read-by-year
[]
(let [res (books-read)
parts (group-by (comp parse-year :when) res)]
(map #(hash-map (first %) (count (second %))) parts))) | ||||||||||||||||||||||||
TODO: datetime parsing | (comment (defn books-in-last [t] (filter #(> ((comp parse-year :when) %) t) (books-read)))) | ||||||||||||||||||||||||
TODO: datetime parsing | (comment (defn recent-books [n] (take n (sort-by :when (complement compare) (books-read))))) | ||||||||||||||||||||||||
(defroutes api-routes (GET "/api/books-read" [] (books-read)) (GET "/api/books-by-year" [] (books-read-by-year))) | |||||||||||||||||||||||||
The Public Website | (ns brian.routes.home
(:require [compojure.core :refer :all]
[brian.views.layout :as layout]
[hiccup.core :refer :all])) | ||||||||||||||||||||||||
Construct markup for icon links. TODO: make data uris for images | (defn icon
[url img]
(let [x [:a {:href url}
[:img.icon {:src img}]]]
x)) | ||||||||||||||||||||||||
(def twitter-icon (icon "//twitter.com/brianru" "/img/twitter.png"))
(def github-icon (icon "//github.com/brianru" "/img/github.png"))
(def linkedin-icon (icon "https://www.linkedin.com/in/brianjrubinton"
"/img/linkedin.png")) | |||||||||||||||||||||||||
Homepage## Top - first impression Full (vertical) page with an opaque background image (that I took). Name in the middle. Most important links directly beneath. ## Middle - depth - data
| (defn home
[]
(layout/common
[:div.top-banner
[:div [:ul
; use initial when viewed from mobile phone
[:li [:span.my-name "Brian James Rubinton"]]
[:li github-icon twitter-icon linkedin-icon]]]])) | ||||||||||||||||||||||||
(defroutes home-routes (GET "/" [] (home))) | |||||||||||||||||||||||||
(ns brian.views.layout (:require [hiccup.page :refer [html5 include-css include-js]])) | |||||||||||||||||||||||||
Template for responsive webpages. ## Includes: - JQuery - Bootstrap | (defn common
[& body]
(html5
[:head
[:meta {:name "viewport"
:content "width=device-width, initial-scale=1"}]
[:title "BJR"]
(include-css "/css/screen.css"
"//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css")
;; TODO D3.js
(include-js "//code.jquery.com/jquery-1.11.0.min.js"
"//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js")]
[:body body])) | ||||||||||||||||||||||||