add "class registry" example app

This commit is contained in:
Gered 2016-05-25 16:26:37 -04:00
parent d5e421de52
commit b8353aaafc
8 changed files with 794 additions and 0 deletions

18
examples/class-registry/.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
.DS_Store
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.settings/
.project
.classpath
.idea/
*.iml
*.ipr
*.iws
/resources/public/cljs

View file

@ -0,0 +1,57 @@
# Reagent Data Views Example - Class Registry
This is a "Class Registry" application that has a lot of CRUD operations
in it which allow users to manage students and professors, as well as
classes and then assign the students/professors to those classes. The
idea is _very_ loosely based off one of the Om tutorial applications
(the data used is almost identical).
[1]: http://reagent-project.github.io/
## Running
### A quick note on the dependencies used
Since Reagent Data Views and the Views library it depends on are all
currently in somewhat of an experimental / pre-beta state right now,
you will need to first clone the following repositories and manually
install the libraries via `lein install`:
* [views](https://github.com/gered/views)
* [views-sql](https://github.com/gered/views-sql)
* [reagent-data-views](https://github.com/gered/reagent-data-views)
As well, you can install [views-honeysql](https://github.com/gered/views-honeysql)
if you want to try out using HoneySQL instead of SQL with views. But
this example app does not use it so it's not required.
### Creating the Database
This example app uses a PostgreSQL database. The SQL script to create
it is in `create_db.sql`. You can easily pipe it into `psql` at a
command line to create it quickly, for example:
$ psql < create_db.sql
(Of course, add any username/host parameters you might need)
### Starting It Up
To build everything and run in one step:
$ lein rundemo
Then open up a web browser or two and head to http://localhost:8080/
to see the web app in action.
If you want to run this application in a REPL, just be sure to build
the ClojureScript:
$ lein cljsbuild once
And then in the REPL you can just run:
(-main)
to start the web app (you should be put in the correct namespace
immediately).

View file

@ -0,0 +1,53 @@
-- For PostgreSQL
-- run with psql. e.g. 'psql < create_db.sql'
CREATE ROLE class_registry LOGIN PASSWORD 's3cr3t';
CREATE DATABASE class_registry OWNER class_registry;
-- assumes you're piping this script into psql ...
\c class_registry;
CREATE TABLE classes
(
class_id SERIAL PRIMARY KEY NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL
);
ALTER TABLE classes OWNER TO class_registry;
CREATE TABLE people
(
people_id SERIAL PRIMARY KEY NOT NULL,
type TEXT NOT NULL,
first_name TEXT NOT NULL,
middle_name TEXT,
last_name TEXT NOT NULL,
email TEXT NOT NULL
);
ALTER TABLE people OWNER TO class_registry;
CREATE TABLE registry
(
registry_id SERIAL PRIMARY KEY NOT NULL,
class_id INTEGER REFERENCES classes (class_id) ON DELETE CASCADE NOT NULL,
people_id INTEGER REFERENCES people (people_id) ON DELETE CASCADE NOT NULL
);
ALTER TABLE registry OWNER TO class_registry;
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('student', 'Ben', NULL, 'Bitdiddle', 'benb@mit.edu');
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('student', 'Alyssa', 'P', 'Hacker', 'aphacker@mit.edu');
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('student', 'Eva', 'Lu', 'Ator', 'eval@mit.edu');
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('student', 'Louis', NULL, 'Reasoner', 'prolog@mit.edu');
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('professor', 'Gerald', 'Jay', 'Sussman', 'metacirc@mit.edu');
INSERT INTO people (type, first_name, middle_name, last_name, email) VALUES ('professor', 'Hal', NULL, 'Abelson', 'evalapply@mit.edu');
INSERT INTO classes (code, name) VALUES ('6001', 'The Structure and Interpretation of Computer Programs');
INSERT INTO classes (code, name) VALUES ('6946', 'The Structure and Interpretation of Classical Mechanics');
INSERT INTO classes (code, name) VALUES ('1806', 'Linear Algebra');
INSERT INTO registry (class_id, people_id) VALUES ((SELECT class_id FROM classes WHERE code = '6001'),
(SELECT people_id FROM people WHERE first_name = 'Gerald' AND middle_name = 'Jay' AND last_name = 'Sussman'));
INSERT INTO registry (class_id, people_id) VALUES ((SELECT class_id FROM classes WHERE code = '6946'),
(SELECT people_id FROM people WHERE first_name = 'Gerald' AND middle_name = 'Jay' AND last_name = 'Sussman'));
INSERT INTO registry (class_id, people_id) VALUES ((SELECT class_id FROM classes WHERE code = '6001'),
(SELECT people_id FROM people WHERE first_name = 'Hal' AND last_name = 'Abelson'));

View file

@ -0,0 +1,56 @@
(defproject class-registry "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.8.51"]
[ring "1.4.0"]
[ring/ring-defaults "0.2.0" :exclusions [javax.servlet/servlet-api]]
[ring-middleware-format "0.7.0"]
[compojure "1.4.0"]
[org.immutant/web "2.1.4"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4.1208.jre7"]
[gered/clj-browserchannel "0.3.2"]
[gered/clj-browserchannel-immutant-adapter "0.0.3"]
[gered/views "1.5-SNAPSHOT"]
[gered/views-sql "0.1.0-SNAPSHOT"]
[reagent-data-views "0.2.0-SNAPSHOT"]
[reagent-data-views-browserchannel "0.1.0-SNAPSHOT"]
[clj-pebble "0.2.0"]
[reagent "0.6.0-alpha2"]
[cljsjs/bootstrap "3.3.6-1"]
[cljs-ajax "0.5.4"]
[environ "1.0.3"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-environ "1.0.3"]]
:main class-registry.server
:clean-targets ^{:protect false} [:target-path
[:cljsbuild :builds :main :compiler :output-dir]
[:cljsbuild :builds :main :compiler :output-to]]
:cljsbuild {:builds {:main
{:source-paths ["src"]
:compiler {:output-to "resources/public/cljs/app.js"
:output-dir "resources/public/cljs/target"
:source-map true
:optimizations :none
:pretty-print true}}}}
:profiles {:dev {:env {:dev "true"}}
:uberjar {:env {}
:aot :all
:hooks [leiningen.cljsbuild]
:cljsbuild {:jar true
:builds {:main
{:compiler ^:replace {:output-to "resources/public/cljs/app.js"
:optimizations :advanced
:pretty-print false}}}}}}
:aliases {"rundemo" ["do" ["clean"] ["cljsbuild" "once"] ["run"]]
"uberjar" ["do" ["clean"] ["uberjar"]]}
)

View file

@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Class Registry</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<!-- CSRF token that ring's anti-forgery middleware is expecting.
we pick it up in class-registry.client and add it to the headers
that cljs-ajax uses -->
<meta name="anti-forgery-token" content="{{ csrfToken }}">
</head>
<body>
<input type="hidden" id="anti-forgery-token" value="{{ csrfToken }}">
<div id="app"></div>
{% if dev %}<script type="text/javascript" src="cljs/target/goog/base.js"></script>{% endif %}
<script type="text/javascript" src="cljs/app.js"></script>
{% if dev %}<script type="text/javascript">goog.require('class_registry.client');</script>{% endif %}
<script type="text/javascript">
class_registry.client.run();
</script>
</body>
</html>

View file

@ -0,0 +1,30 @@
body {
margin-top: 30px;
margin-bottom: 30px;
}
#app-title {
margin-top: 0px;
}
.list div.row {
padding-top: 5px;
}
.list div.row:hover {
background-color: #d9edf7;
}
.class-registry {
margin-top: 3px;
margin-bottom: 0px;
}
div.actions button {
margin-right: 5px;
}
div.row .value {
padding-top: 6px;
padding-bottom: 6px;
}

View file

@ -0,0 +1,382 @@
(ns class-registry.client
(:require
[clojure.string :as string]
[reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]]
[net.thegeez.browserchannel.client :as browserchannel]
[reagent-data-views.client.component :refer [view-cursor] :refer-macros [defvc]]
[reagent-data-views.browserchannel.client :as rdv-browserchannel]))
;; Class Registry - Reagent Data Views example app
;;
;; (This example app is (very) loosely based on one of the examples in the Om tutorial).
;;
;; This application is a little bit more complex for an example app, mostly caused by
;; the UI layout (in place editing, etc). Even still, for anyone well versed in Reagent
;; this code shouldn't be difficult to follow.
;;
;; In a real-world application we probably would not want to be quite this lazy when
;; it comes to usage of view-cursor. However, for an example app I think this does
;; serve to demonstrate how you can very quickly build UI's which reactively update to
;; backend database operations and get a lot of "free" UI refreshes, simplifying your
;; code.
;; AJAX actions
(defn add-person! [person] (POST "/people/add" {:params {:person person}}))
(defn save-person! [person] (POST "/people/update" {:params {:person person}}))
(defn delete-person! [id] (POST "/people/delete" {:params {:id id}}))
(defn add-class! [class] (POST "/class/add" {:params {:class class}}))
(defn save-class! [class] (POST "/class/update" {:params {:class class}}))
(defn delete-class! [id] (POST "/class/delete" {:params {:id id}}))
(defn add-registration!
[class-id people-id]
(POST "/registry/add" {:params {:class-id class-id :people-id people-id}}))
(defn remove-registration!
[id]
(POST "/registry/remove" {:params {:id id}}))
;; helper/utility functions
(defn parse-person-name
"returns a map with a person's first/last and optionally middle name in it
when given a string of the format 'first-name middle-name last-name' or
'first-name last-name'. the middle name can also just be an initial."
[formatted-full-name]
(let [[first middle last :as parts] (string/split formatted-full-name #"\s+")
[first last middle] (if (nil? last) [first middle] [first last middle])
middle (when middle (string/replace middle "." ""))]
(if (>= (count parts) 2)
{:first_name first
:last_name last
:middle_name middle})))
(defn format-editable-name
"does the reverse of parse-person-name"
[{:keys [first_name middle_name last_name] :as person}]
(if middle_name
(str first_name " " middle_name " " last_name)
(str first_name " " last_name)))
(defn display-middle-name
[{:keys [middle_name] :as person}]
(if (= (count middle_name) 1)
(str " " middle_name ".")
(str " " middle_name)))
(defn display-name
[{:keys [first_name last_name] :as person}]
(str last_name ", " first_name (display-middle-name person)))
(defn validate-person
[person]
(cond
(or (string/blank? (:first_name person))
(string/blank? (:last_name person)))
(js/alert "Invalid format for person's name. Format is:\n\n\"first_name [middle_name] last_name\"")
(string/blank? (:email person))
(js/alert "Email address is required.")
:else person))
(defn validate-class
[class]
(cond
(string/blank? (:code class))
(js/alert "Class code is required.")
(string/blank? (:name class))
(js/alert "Class name is required.")
:else class))
;; UI helpers
(defn text-edit
"text editing component which maintains the text value state in a key within a provided atom"
[& args]
(let [[attrs value k] (if (= (count args) 2) (cons {} args) args)]
[:input.form-control
(merge
attrs
{:type "text"
:value (get @value k)
:on-change #(swap! value assoc k (-> % .-target .-value))})]))
(defn dropdown-list
"dropdown list / select component which maintains selection state in a provided atom"
[& args]
(let [[attrs args] (if (= (count args) 2) args (cons {} args))
{:keys [value data value-fn label-fn placeholder default-value]} args
options (map
(fn [item]
^{:key (value-fn item)}
[:option {:value (value-fn item)} (label-fn item)])
data)
options (if placeholder
(cons ^{:key default-value}
[:option {:value default-value} placeholder]
options)
options)]
[:select.form-control
(merge
attrs
{:value @value
:on-change #(reset! value (-> % .-target .-value))})
options]))
;; People UI
(defn person-info
"row showing a single person and allowing editing/removal of it"
[]
(let [editing (r/atom nil)
end-editing! #(reset! editing nil)
start-editing! (fn [{:keys [people_id email] :as person}]
(reset! editing
{:id people_id
:email email
:name (format-editable-name person)}))
save! (fn []
(let [person (-> (:name @editing)
(parse-person-name)
(merge (select-keys @editing [:id :email])))]
(when (validate-person person)
(save-person! person)
(end-editing!))))
delete! (fn [{:keys [people_id] :as person}]
(delete-person! people_id))]
(fn [{:keys [email] :as person}]
(if @editing
[:div.row.bg-warning
[:div.col-sm-5 [text-edit editing :name]]
[:div.col-sm-5 [text-edit editing :email]]
[:div.col-sm-2.actions
[:button.btn.btn-sm.btn-success {:on-click save!} [:span.glyphicon.glyphicon-ok]]
[:button.btn.btn-sm.btn-default {:on-click end-editing!} [:span.glyphicon.glyphicon-remove]]]]
; not-editing display
[:div.row
[:div.col-sm-5 [:div.value (display-name person)]]
[:div.col-sm-5 [:div.value email]]
[:div.col-sm-2.actions
[:button.btn.btn-sm.btn-default {:on-click #(start-editing! person)} [:span.glyphicon.glyphicon-pencil]]
[:button.btn.btn-sm.btn-danger {:on-click #(delete! person)} [:span.glyphicon.glyphicon-remove]]]]))))
(defn new-person
"row showing entry form for adding a new person"
[type]
(let [values (r/atom {})
add! (fn []
(let [person (merge {:type type
:email (:email @values)}
(parse-person-name (:name @values)))]
(when (validate-person person)
(add-person! person)
(reset! values {}))))]
[:div.row
[:div.col-sm-5 [text-edit {:placeholder "Full name"} values :name]]
[:div.col-sm-5 [text-edit {:placeholder "Email"} values :email]]
[:div.col-sm-2.actions
[:button.btn.btn-sm.btn-primary {:on-click add!} [:span.glyphicon.glyphicon-plus]]]]))
(defn people-list
"sub-container for showing list of people of a certain type and allowing entry
of new people of that same type"
[people-type people]
[:div.container-fluid.list
(map
(fn [{:keys [people_id] :as person}]
^{:key people_id} [person-info person])
people)
[new-person people-type]])
(defvc people
"main container for people information"
[]
(let [professors (view-cursor :people "professor")
students (view-cursor :people "student")]
[:div#people.container-fluid
[:div.panel.panel-default
[:div.panel-heading [:h3.panel-title "Professors"]]
[:div.panel-body [people-list "professor" @professors]]]
[:div.panel.panel-default
[:div.panel-heading [:h3.panel-title "Students"]]
[:div.panel-body [people-list "student" @students]]]]))
;; Class Registry UI
(defn registration-info
"row showing a class registration and allowing removal of it"
[{:keys [registry_id type] :as registration}]
[:div.row
[:div.col-sm-2.value (string/capitalize type)]
[:div.col-sm-9.value (display-name registration)]
[:div.col-sm-1.actions
[:button.btn.btn-sm.btn-danger {:on-click #(remove-registration! registry_id)}
[:span.glyphicon.glyphicon-remove]]]])
(defvc new-registration
"row showing entry form for registering someone into a class"
[]
(let [value (r/atom "")]
(fn [class-id]
(let [people (view-cursor :people-registerable-for-class class-id)
add! #(if-let [person-id @value]
(when-not (= "" person-id)
(add-registration! class-id (js/parseInt person-id))
(reset! value "")))]
[:div.row
[:div.col-sm-11
[dropdown-list
{:data @people
:value value
:value-fn :people_id
:label-fn #(str
(if (= "professor" (:type %)) "(Professor) ")
(display-name %))
:placeholder "(Select a person to add!)"
:default-value ""}]]
[:div.col-sm-1.actions
[:button.btn.btn-sm.btn-primary {:on-click add!} [:span.glyphicon.glyphicon-plus]]]]))))
(defvc class-registry-list
"sub-container for showing list of people of a certain type and allowing entry
of new people of that same type"
[{:keys [class_id code name] :as class}]
(let [registry (view-cursor :class-registry class_id)]
[:div.col-sm-12.panel.panel-default.class-registry
[:div.panel-body
[:h4 "Class Registration"]
(map
(fn [{:keys [registry_id] :as registration}]
^{:key registry_id} [registration-info registration])
@registry)
[new-registration class_id]]]))
;; Classes UI
(defn class-info
"row showing a single class and allowing editing/removal of it. also has a
toggle to show/hide the people registered in the class"
[]
(let [editing (r/atom nil)
show-registry? (r/atom false)
end-editing! #(reset! editing nil)
start-editing! (fn [class]
(reset! editing class)
(reset! show-registry? false))
save! #(let [class @editing]
(when (validate-class class)
(save-class! class)
(end-editing!)))
delete! #(delete-class! (:class_id %))
toggle-registry! #(swap! show-registry? not)]
(fn [{:keys [code name] :as class}]
(if @editing
[:div.row.bg-warning
[:div.col-sm-2 [text-edit editing :code]]
[:div.col-sm-7 [text-edit editing :name]]
[:div.col-sm-3.actions
[:button.btn.btn-sm.btn-success {:on-click save!} [:span.glyphicon.glyphicon-ok]]
[:button.btn.btn-sm.btn-default {:on-click end-editing!} [:span.glyphicon.glyphicon-remove]]]]
; not-editing display
[:div
{:class (str "row" (if @show-registry? " bg-success"))
:on-double-click toggle-registry!}
[:div.col-sm-2.value code]
[:div.col-sm-7.value name]
[:div.col-sm-3.actions
[:button.btn.btn-sm.btn-default {:on-click #(start-editing! class)} [:span.glyphicon.glyphicon-pencil]]
[:button.btn.btn-sm.btn-danger {:on-click #(delete! class)} [:span.glyphicon.glyphicon-remove]]
[:button
{:on-click toggle-registry!
:class (str "btn btn-sm btn-info" (if @show-registry? " active"))}
[:span.glyphicon.glyphicon-user]]]
(if @show-registry?
[class-registry-list class])]))))
(defn new-class
"row showing entry form for adding a new class"
[]
(let [values (r/atom {})
add! (fn []
(let [class @values]
(when (validate-class class)
(add-class! class)
(reset! values {}))))]
(fn []
[:div.row
[:div.col-sm-2 [text-edit {:placeholder "Code"} values :code]]
[:div.col-sm-7 [text-edit {:placeholder "Name"} values :name]]
[:div.col-sm-3.actions
[:button.btn.btn-sm.btn-primary {:on-click add!} [:span.glyphicon.glyphicon-plus]]]])))
(defn class-list
"sub-container for showing list of classes and allowing entry of new classes"
[classes]
[:div.container-fluid.list
(map
(fn [{:keys [class_id] :as class}]
^{:key class_id} [class-info class])
classes)
[new-class]])
(defvc classes
"main container for class information"
[]
(let [classes (view-cursor :classes)]
[:div#classes.container-fluid
[:div.panel.panel-default
[:div.panel-heading [:h3.panel-title "Classes"]]
[:div.panel-body [class-list @classes]]]]))
(defn class-registry-app
"main application container"
[]
[:div.container-fluid
[:h1#app-title.page-header "Class Registry " [:small "Reagent Data Views Example"]]
[:div.row
[:div.col-sm-6 [people]]
[:div.col-sm-6 [classes]]]])
;; AJAX CSRF stuff
(defn get-anti-forgery-token
[]
(if-let [tag (aget (.querySelectorAll js/document "meta[name='anti-forgery-token']") 0)]
(.-content tag)))
(def csrf-interceptor
(to-interceptor {:name "CSRF Interceptor"
:request #(assoc-in % [:headers "X-CSRF-Token"] (get-anti-forgery-token))}))
(swap! default-interceptors (partial cons csrf-interceptor))
;; Page load
(defn ^:export run
[]
(enable-console-print!)
(rdv-browserchannel/configure!)
(browserchannel/connect! {} {:middleware [rdv-browserchannel/middleware]})
(r/render-component [class-registry-app] (.getElementById js/document "app")))

View file

@ -0,0 +1,173 @@
(ns class-registry.server
(:gen-class)
(:require
[compojure.core :refer [routes GET POST]]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
[ring.middleware.format :refer [wrap-restful-format]]
[ring.util.response :refer [response]]
[net.thegeez.browserchannel.server :refer [wrap-browserchannel]]
[net.thegeez.browserchannel.immutant-async-adapter :refer [wrap-immutant-async-adapter]]
[immutant.web :as immutant]
[clj-pebble.core :as pebble]
[environ.core :refer [env]]
[clojure.java.jdbc :as jdbc]
[views.sql.core :refer [vexec! with-view-transaction]]
[views.sql.view :refer [view]]
[reagent-data-views.browserchannel.server :as rdv-browserchannel]))
(def dev? (boolean (env :dev)))
(def db {:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:subname "//10.0.0.20/class_registry"
:user "class_registry"
:password "s3cr3t"})
;; View functions.
(defn classes-list
[]
["SELECT class_id, code, name
FROM classes
ORDER BY code"])
(defn people-list
[& [type]]
(if type
["SELECT people_id, type, first_name, middle_name, last_name, email
FROM people
WHERE type = ?
ORDER BY last_name, first_name"
type]
["SELECT people_id, type, first_name, middle_name, last_name, email
FROM people
ORDER BY type, last_name, first_name"]))
(defn class-registry
[class-id]
["SELECT r.registry_id, p.type, p.first_name, p.middle_name, p.last_name
FROM registry r
JOIN people p on p.people_id = r.people_id
WHERE r.class_id = ?
ORDER BY p.type, p.last_name, p.first_name"
class-id])
(defn people-registerable-for-class
[class-id]
["SELECT p.people_id, p.type, p.first_name, p.middle_name, p.last_name, p.email
FROM people p
WHERE p.people_id NOT IN (SELECT r.people_id
FROM registry r
WHERE r.class_id = ?)"
class-id])
;; Views list.
(def views
[(view :classes db #'classes-list)
(view :people db #'people-list)
(view :class-registry db #'class-registry)
(view :people-registerable-for-class db #'people-registerable-for-class)])
;; SQL operations triggered by AJAX requests.
(defn add-person!
[{:keys [type first_name middle_name last_name email] :as person}]
(vexec! db ["INSERT INTO people (type, first_name, middle_name, last_name, email)
VALUES (?, ?, ?, ?, ?)"
type first_name middle_name last_name email])
(response "ok"))
(defn update-person!
[{:keys [id first_name middle_name last_name email] :as person}]
(vexec! db ["UPDATE people SET
first_name = ?, middle_name = ?, last_name = ?, email = ?
WHERE people_id = ?"
first_name middle_name last_name email id])
(response "ok"))
(defn delete-person!
[id]
(vexec! db ["DELETE FROM people WHERE people_id = ?" id])
(response "ok"))
(defn add-class!
[{:keys [code name] :as class}]
(vexec! db ["INSERT INTO classes (code, name) VALUES (?, ?)" code, name])
(response "ok"))
(defn update-class!
[{:keys [class_id code name] :as class}]
(vexec! db ["UPDATE classes SET code = ?, name = ? WHERE class_id = ?" code name class_id])
(response "ok"))
(defn delete-class!
[id]
(vexec! db ["DELETE FROM classes WHERE class_id = ?" id])
(response "ok"))
(defn add-registration!
[class-id people-id]
(vexec! db ["INSERT INTO registry (class_id, people_id) VALUES (?, ?)" class-id people-id])
(response "ok"))
(defn remove-registration!
[registry-id]
(vexec! db ["DELETE FROM registry WHERE registry_id = ?" registry-id])
(response "ok"))
;; Compojure routes and Ring handler
(def app-routes
(routes
; ajax db action routes
(POST "/people/add" [person] (add-person! person))
(POST "/people/update" [person] (update-person! person))
(POST "/people/delete" [id] (delete-person! id))
(POST "/class/add" [class] (add-class! class))
(POST "/class/update" [class] (update-class! class))
(POST "/class/delete" [id] (delete-class! id))
(POST "/registry/add" [class-id people-id] (add-registration! class-id people-id))
(POST "/registry/remove" [id] (remove-registration! id))
; main page
(GET "/" [] (pebble/render-resource
"html/app.html"
{:dev dev?
:csrfToken *anti-forgery-token*}))
(route/resources "/")
(route/not-found "not found")))
(def handler
(-> app-routes
(wrap-restful-format :formats [:transit-json])
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?)))
(wrap-browserchannel {} {:middleware [rdv-browserchannel/middleware]})
(wrap-immutant-async-adapter)))
;; Web server startup & main
(defn run-server []
(pebble/set-options! :cache (not dev?))
(rdv-browserchannel/init-views! views)
(immutant/run handler {:port 8080}))
(defn -main [& args]
(run-server))