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])) | ||||||||||||||||||||||||