brian

0.1.3


a web app for data heavy personal websites

dependencies

org.clojure/clojure
1.6.0
compojure
1.1.6
hiccup
1.0.5
ring-server
0.3.1
ring/ring-json
0.3.1
fogus/bacwn
0.4.0
clj-time
0.8.0
clojure-csv/clojure-csv
2.0.1



(this space intentionally left almost blank)
 

The App

The application can be split into the following verticals:

  • Public Website (home-routes)
  • Public API (api-routes)
  • Private Website (tbd)
(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 Data

Initialize, load and save the in-memory database.

Why datalog?

  • I want to learn datalog.
  • I want to learn datomic, which uses datalog.
  • Queries are data.
  • Bacwn is a small datalog implementation that I can squeeze into my head.
  • I know SQL. I don't know 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 (old-ks) then swap the old keys (old-ks) for the new keys (new-ks).

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

#'app expands to (var app) so that when we reload our code, the server is forced to re-resolve the symbol in the var rather than having its own copy. When the root binding changes, the server picks it up without having to restart.

(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

  • reading habits (shelfari)
  • fitness (fitbit)
  • programming (github)

    Bottom - tbd

  • contact information?

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