Added Luminus app skeleton; added ADL.
This commit is contained in:
parent
8a923c1b33
commit
f3c0e728a4
28
Capstanfile
Normal file
28
Capstanfile
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
#
|
||||
# Name of the base image. Capstan will download this automatically from
|
||||
# Cloudius S3 repository.
|
||||
#
|
||||
#base: cloudius/osv
|
||||
base: cloudius/osv-openjdk8
|
||||
|
||||
#
|
||||
# The command line passed to OSv to start up the application.
|
||||
#
|
||||
cmdline: /java.so -jar /pastoralist/app.jar
|
||||
|
||||
#
|
||||
# The command to use to build the application.
|
||||
# You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine
|
||||
#
|
||||
# For Leiningen, you can use:
|
||||
#build: lein uberjar
|
||||
# For Boot, you can use:
|
||||
#build: boot build
|
||||
|
||||
#
|
||||
# List of files that are included in the generated image.
|
||||
#
|
||||
files:
|
||||
/pastoralist/app.jar: ./target/uberjar/pastoralist.jar
|
||||
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM openjdk:8-alpine
|
||||
|
||||
COPY target/uberjar/pastoralist.jar /pastoralist/app.jar
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["java", "-jar", "/pastoralist/app.jar"]
|
1
Procfile
Normal file
1
Procfile
Normal file
|
@ -0,0 +1 @@
|
|||
web: java -cp target/uberjar/pastoralist.jar clojure.main -m pastoralist.core
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# pastoralist
|
||||
|
||||
generated using Luminus version "3.54"
|
||||
|
||||
FIXME
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need [Leiningen][1] 2.0 or above installed.
|
||||
|
||||
[1]: https://github.com/technomancy/leiningen
|
||||
|
||||
## Running
|
||||
|
||||
To start a web server for the application, run:
|
||||
|
||||
lein run
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2019 FIXME
|
16
dev-config.edn
Normal file
16
dev-config.edn
Normal file
|
@ -0,0 +1,16 @@
|
|||
;; WARNING
|
||||
;; The dev-config.edn file is used for local environment variables, such as database credentials.
|
||||
;; This file is listed in .gitignore and will be excluded from version control by Git.
|
||||
|
||||
{:dev true
|
||||
:port 3000
|
||||
;; when :nrepl-port is set the application starts the nREPL server on load
|
||||
:nrepl-port 7000
|
||||
|
||||
;;Twitter used as an example, replace these URIs with the OAuth provider of your choice
|
||||
:request-token-uri "https://api.twitter.com/oauth/request_token"
|
||||
:access-token-uri "https://api.twitter.com/oauth/access_token"
|
||||
:authorize-uri "https://api.twitter.com/oauth/authenticate"
|
||||
; set your dev database connection URL here
|
||||
; :database-url "postgresql://localhost/pastoralist_dev?user=simon&password=db_user_password_here"
|
||||
}
|
77
env/dev/clj/pastoralist/core.clj
vendored
Normal file
77
env/dev/clj/pastoralist/core.clj
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
(ns pastoralist.core
|
||||
(:require
|
||||
[pastoralist.handler :as handler]
|
||||
[pastoralist.nrepl :as nrepl]
|
||||
[luminus.http-server :as http]
|
||||
[luminus-migrations.core :as migrations]
|
||||
[pastoralist.config :refer [env]]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :as mount])
|
||||
(:gen-class))
|
||||
|
||||
;; log uncaught exceptions in threads
|
||||
(Thread/setDefaultUncaughtExceptionHandler
|
||||
(reify Thread$UncaughtExceptionHandler
|
||||
(uncaughtException [_ thread ex]
|
||||
(log/error {:what :uncaught-exception
|
||||
:exception ex
|
||||
:where (str "Uncaught exception on" (.getName thread))}))))
|
||||
|
||||
(def cli-options
|
||||
[["-p" "--port PORT" "Port number"
|
||||
:parse-fn #(Integer/parseInt %)]])
|
||||
|
||||
(mount/defstate ^{:on-reload :noop} http-server
|
||||
:start
|
||||
(http/start
|
||||
(-> env
|
||||
(assoc :handler (handler/app))
|
||||
(update :io-threads #(or % (* 2 (.availableProcessors (Runtime/getRuntime)))))
|
||||
(update :port #(or (-> env :options :port) %))))
|
||||
:stop
|
||||
(http/stop http-server))
|
||||
|
||||
(mount/defstate ^{:on-reload :noop} repl-server
|
||||
:start
|
||||
(when (env :nrepl-port)
|
||||
(nrepl/start {:bind (env :nrepl-bind)
|
||||
:port (env :nrepl-port)}))
|
||||
:stop
|
||||
(when repl-server
|
||||
(nrepl/stop repl-server)))
|
||||
|
||||
|
||||
(defn init-jndi []
|
||||
(System/setProperty "java.naming.factory.initial"
|
||||
"org.apache.naming.java.javaURLContextFactory")
|
||||
(System/setProperty "java.naming.factory.url.pkgs"
|
||||
"org.apache.naming"))
|
||||
|
||||
(defn start-app [args]
|
||||
(init-jndi)
|
||||
(doseq [component (-> args
|
||||
(parse-opts cli-options)
|
||||
mount/start-with-args
|
||||
:started)]
|
||||
(log/info component "started"))
|
||||
(.addShutdownHook (Runtime/getRuntime) (Thread. handler/destroy)))
|
||||
|
||||
(defn -main [& args]
|
||||
(mount/start #'pastoralist.config/env)
|
||||
(cond
|
||||
(nil? (:database-url env))
|
||||
(do
|
||||
(log/error "Database configuration not found, :database-url environment variable must be set before running")
|
||||
(System/exit 1))
|
||||
(some #{"init"} args)
|
||||
(do
|
||||
(migrations/init (select-keys env [:database-url :init-script]))
|
||||
(System/exit 0))
|
||||
(migrations/migration? args)
|
||||
(do
|
||||
(migrations/migrate args (select-keys env [:database-url]))
|
||||
(System/exit 0))
|
||||
:else
|
||||
(start-app args)))
|
||||
|
11
env/dev/clj/pastoralist/dev_middleware.clj
vendored
Normal file
11
env/dev/clj/pastoralist/dev_middleware.clj
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
(ns pastoralist.dev-middleware
|
||||
(:require
|
||||
[ring.middleware.reload :refer [wrap-reload]]
|
||||
[selmer.middleware :refer [wrap-error-page]]
|
||||
[prone.middleware :refer [wrap-exceptions]]))
|
||||
|
||||
(defn wrap-dev [handler]
|
||||
(-> handler
|
||||
wrap-reload
|
||||
wrap-error-page
|
||||
(wrap-exceptions {:app-namespaces ['pastoralist]})))
|
15
env/dev/clj/pastoralist/env.clj
vendored
Normal file
15
env/dev/clj/pastoralist/env.clj
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
(ns pastoralist.env
|
||||
(:require
|
||||
[selmer.parser :as parser]
|
||||
[clojure.tools.logging :as log]
|
||||
[pastoralist.dev-middleware :refer [wrap-dev]]))
|
||||
|
||||
(def defaults
|
||||
{:init
|
||||
(fn []
|
||||
(parser/cache-off!)
|
||||
(log/info "\n-=[pastoralist started successfully using the development profile]=-"))
|
||||
:stop
|
||||
(fn []
|
||||
(log/info "\n-=[pastoralist has shut down successfully]=-"))
|
||||
:middleware wrap-dev})
|
12
env/dev/clj/pastoralist/figwheel.clj
vendored
Normal file
12
env/dev/clj/pastoralist/figwheel.clj
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
(ns pastoralist.figwheel
|
||||
(:require [figwheel-sidecar.repl-api :as ra]))
|
||||
|
||||
(defn start-fw []
|
||||
(ra/start-figwheel!))
|
||||
|
||||
(defn stop-fw []
|
||||
(ra/stop-figwheel!))
|
||||
|
||||
(defn cljs []
|
||||
(ra/cljs-repl))
|
||||
|
64
env/dev/clj/user.clj
vendored
Normal file
64
env/dev/clj/user.clj
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
(ns user
|
||||
"Userspace functions you can run by default in your local REPL."
|
||||
(:require
|
||||
[pastoralist.config :refer [env]]
|
||||
[clojure.pprint]
|
||||
[clojure.spec.alpha :as s]
|
||||
[expound.alpha :as expound]
|
||||
[mount.core :as mount]
|
||||
[pastoralist.figwheel :refer [start-fw stop-fw cljs]]
|
||||
[pastoralist.core :refer [start-app]]
|
||||
[pastoralist.db.core]
|
||||
[conman.core :as conman]
|
||||
[luminus-migrations.core :as migrations]))
|
||||
|
||||
(alter-var-root #'s/*explain-out* (constantly expound/printer))
|
||||
|
||||
(add-tap (bound-fn* clojure.pprint/pprint))
|
||||
|
||||
(defn start
|
||||
"Starts application.
|
||||
You'll usually want to run this on startup."
|
||||
[]
|
||||
(mount/start-without #'pastoralist.core/repl-server))
|
||||
|
||||
(defn stop
|
||||
"Stops application."
|
||||
[]
|
||||
(mount/stop-except #'pastoralist.core/repl-server))
|
||||
|
||||
(defn restart
|
||||
"Restarts application."
|
||||
[]
|
||||
(stop)
|
||||
(start))
|
||||
|
||||
(defn restart-db
|
||||
"Restarts database."
|
||||
[]
|
||||
(mount/stop #'pastoralist.db.core/*db*)
|
||||
(mount/start #'pastoralist.db.core/*db*)
|
||||
(binding [*ns* 'pastoralist.db.core]
|
||||
(conman/bind-connection pastoralist.db.core/*db* "sql/queries.sql")))
|
||||
|
||||
(defn reset-db
|
||||
"Resets database."
|
||||
[]
|
||||
(migrations/migrate ["reset"] (select-keys env [:database-url])))
|
||||
|
||||
(defn migrate
|
||||
"Migrates database up for all outstanding migrations."
|
||||
[]
|
||||
(migrations/migrate ["migrate"] (select-keys env [:database-url])))
|
||||
|
||||
(defn rollback
|
||||
"Rollback latest database migration."
|
||||
[]
|
||||
(migrations/migrate ["rollback"] (select-keys env [:database-url])))
|
||||
|
||||
(defn create-migration
|
||||
"Create a new up and down migration file with a generated timestamp and `name`."
|
||||
[name]
|
||||
(migrations/create name (select-keys env [:database-url])))
|
||||
|
||||
|
19
env/dev/cljs/pastoralist/app.cljs
vendored
Normal file
19
env/dev/cljs/pastoralist/app.cljs
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
(ns^:figwheel-no-load pastoralist.app
|
||||
(:require
|
||||
[pastoralist.core :as core]
|
||||
[cljs.spec.alpha :as s]
|
||||
[expound.alpha :as expound]
|
||||
[devtools.core :as devtools]))
|
||||
|
||||
(extend-protocol IPrintWithWriter
|
||||
js/Symbol
|
||||
(-pr-writer [sym writer _]
|
||||
(-write writer (str "\"" (.toString sym) "\""))))
|
||||
|
||||
(set! s/*explain-out* expound/printer)
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
(devtools/install!)
|
||||
|
||||
(core/init! true)
|
1
env/dev/resources/config.edn
vendored
Normal file
1
env/dev/resources/config.edn
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
35
env/dev/resources/logback.xml
vendored
Normal file
35
env/dev/resources/logback.xml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>log/pastoralist.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>log/pastoralist.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<!-- keep 30 days of history -->
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="org.apache.http" level="warn" />
|
||||
<logger name="org.xnio.nio" level="warn" />
|
||||
<logger name="com.zaxxer.hikari" level="warn" />
|
||||
<logger name="org.eclipse.jetty" level="warn" />
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
11
env/prod/clj/pastoralist/env.clj
vendored
Normal file
11
env/prod/clj/pastoralist/env.clj
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
(ns pastoralist.env
|
||||
(:require [clojure.tools.logging :as log]))
|
||||
|
||||
(def defaults
|
||||
{:init
|
||||
(fn []
|
||||
(log/info "\n-=[pastoralist started successfully]=-"))
|
||||
:stop
|
||||
(fn []
|
||||
(log/info "\n-=[pastoralist has shut down successfully]=-"))
|
||||
:middleware identity})
|
7
env/prod/cljs/pastoralist/app.cljs
vendored
Normal file
7
env/prod/cljs/pastoralist/app.cljs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
(ns pastoralist.app
|
||||
(:require [pastoralist.core :as core]))
|
||||
|
||||
;;ignore println statements in prod
|
||||
(set! *print-fn* (fn [& _]))
|
||||
|
||||
(core/init! false)
|
2
env/prod/resources/config.edn
vendored
Normal file
2
env/prod/resources/config.edn
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{:prod true
|
||||
:port 3000}
|
26
env/prod/resources/logback.xml
vendored
Normal file
26
env/prod/resources/logback.xml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>log/pastoralist.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>log/pastoralist.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<!-- keep 30 days of history -->
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="org.apache.http" level="warn" />
|
||||
<logger name="org.xnio.nio" level="warn" />
|
||||
<logger name="com.zaxxer.hikari" level="warn" />
|
||||
<logger name="org.eclipse.jetty" level="warn" />
|
||||
<root level="INFO">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
1
env/test/resources/config.edn
vendored
Normal file
1
env/test/resources/config.edn
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
35
env/test/resources/logback.xml
vendored
Normal file
35
env/test/resources/logback.xml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>log/pastoralist.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>log/pastoralist.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<!-- keep 30 days of history -->
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="org.apache.http" level="warn" />
|
||||
<logger name="org.xnio.nio" level="warn" />
|
||||
<logger name="com.zaxxer.hikari" level="warn" />
|
||||
<logger name="org.eclipse.jetty" level="warn" />
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
197
pastoralist.adl.xml
Normal file
197
pastoralist.adl.xml
Normal file
|
@ -0,0 +1,197 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- DOCTYPE application PUBLIC "-//JOURNEYMAN//DTD ADL 1.4.1//EN"
|
||||
"http://www.journeyman.cc/adl/stable/adl/schemas/adl-1.4.1.dtd" -->
|
||||
<application xmlns="http://bowyer.journeyman.cc/adl/1.4.1/"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns:adl="http://bowyer.journeyman.cc/adl/1.4.1/" name="pastoralist"
|
||||
version="0.0.1">
|
||||
|
||||
<documentation>A web-app intended to be used by pastoralists in managing
|
||||
pastures, grazing, and animals.</documentation>
|
||||
|
||||
<entity name="holding" magnitude="7" volatility="1">
|
||||
<documentation>All holdings used to the system</documentation>
|
||||
<key>
|
||||
<documentation>Although in the UK and, probably, in many other
|
||||
countries, holdings have a unique, government issued, holding
|
||||
number which could rationally be used as a key, internationally
|
||||
we cannot rely on this.
|
||||
</documentation>
|
||||
<property required="true" type="integer" name="id"
|
||||
column="id" immutable="true" distinct="system">
|
||||
<generator action="native"/>
|
||||
</property>
|
||||
</key>
|
||||
<property size="64" type="string" name="holding-identifier" distinct="all">
|
||||
<prompt prompt="Holding Number" locale="en_GB.UTF-8" immutable="true"/>
|
||||
</property>
|
||||
<property size="128" type="string" name="name" distinct="user"/>
|
||||
<property type="list" name="pastures" entity="pasture"/>
|
||||
<property type="list" name="animals" entity="animal"/>
|
||||
<property type="link" name="users" entity="user">
|
||||
<documentation>A user is a person authorised to see data on a holding.
|
||||
Some users (e.g. vets, farm secretaries) may be authorised to see data
|
||||
on many holdings.</documentation>
|
||||
</property>
|
||||
<property name="is-active" type="boolean" default="true">
|
||||
<documentation>Records are not normally deleted from this system because
|
||||
of historical data integrity; we may want to trace the history of an
|
||||
animal across holdings even after a holding has ceased to be active
|
||||
in the system.</documentation>
|
||||
</property>
|
||||
</entity>
|
||||
|
||||
<entity name="pasture" magnitude="8" volatility="2">
|
||||
<documentation>A pasture within a holding. TODO: how should common pastures,
|
||||
or pastures otherwise shared between holdings, be handled?</documentation>
|
||||
<key>
|
||||
<property required="true" type="integer" name="id"
|
||||
column="id" immutable="true" distinct="system">
|
||||
<generator action="native"/>
|
||||
</property>
|
||||
</key>
|
||||
<property size="128" type="string" name="name" distinct="user"/>
|
||||
<property type="real" name="area" distinct="user">
|
||||
<prompt prompt="Area in Ha" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property name="holding" type="entity" entity="holding" required="true"/>
|
||||
<property name="geometry" type="entity" entity="polygon" required="true">
|
||||
<documentation>
|
||||
Polygon is a first-class datatype in PostGIS, but ADL does not yet
|
||||
support this. Solutions:
|
||||
1. hack ADL to support native polygons;
|
||||
2. implement our own polygon datatype;
|
||||
3. work-around in override code.
|
||||
|
||||
Generally, when pasture boundaries are changed, a pasture record
|
||||
should not be edited; rather it should be marked inactive, and new
|
||||
pasture record(s) created.
|
||||
</documentation>
|
||||
</property>
|
||||
<property name="subdivision-of" type="entity" entity="pasture"/>
|
||||
<property name="is-active" type="boolean" default="true">
|
||||
<documentation>Records are not normally deleted from this system because
|
||||
of historical data integrity; we may want to trace the history of an
|
||||
pasture even after pasture boundaries have been changed.
|
||||
|
||||
Indeed, if we treat subdivisions as pastures (which would be quite sensible),
|
||||
active pastures in the system will change quite frequently, especially if
|
||||
strip-grazing is used.
|
||||
</documentation>
|
||||
</property>
|
||||
<property name="history" type="link" entity="event"/>
|
||||
</entity>
|
||||
|
||||
<entity name="animal" magnitude="9" volatility="5">
|
||||
<documentation>An animal on a holding (or which has been on a holding; the
|
||||
historical record is not wiped when an animal dies, is killed, or is sold)
|
||||
</documentation>
|
||||
<key>
|
||||
<documentation>Although in the UK and, probably, in many other
|
||||
countries, animals have a unique, government registered, identifying
|
||||
number which could rationally be used as a key, internationally
|
||||
we cannot rely on this.
|
||||
</documentation>
|
||||
<property required="true" type="integer" name="id"
|
||||
column="id" immutable="true" distinct="system">
|
||||
<generator action="native"/>
|
||||
</property>
|
||||
</key>
|
||||
<property name="holding" type="entity" entity="holding">
|
||||
<documentation>The holding the animal is currently on, if any. Not 'required',
|
||||
since the animal may be dead, or simply moved off to a holding which is
|
||||
not part of the system.</documentation>
|
||||
</property>
|
||||
<property name="dam" type="entity" entity="animal"/>
|
||||
<property name="sire" type="entity" entity="animal"/>
|
||||
<property name="born" type="date" immutable="true" required="true"/>
|
||||
<property name="animal-identifier" type="string" size="64" distinct="user" required="true">
|
||||
<prompt prompt="Ear-tag Number" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property name="gender" type="entity" entity="gender" required="true"/>
|
||||
<property name="species" type="entity" entity="species" required="true"/>
|
||||
<property name="breed" type="entity" entity="breed"/>
|
||||
<property name="is-pedigree" type="boolean" default="false"/>
|
||||
<property name="pasture" type="entity" entity="pasture"/>
|
||||
<property name="history" type="link" entity="event"/>
|
||||
</entity>
|
||||
|
||||
<entity name="gender" magnitude="1" volatility="10">
|
||||
<key>
|
||||
<property name="gender" type="string" size="8" distinct="all" immutable="true"/>
|
||||
</key>
|
||||
</entity>
|
||||
|
||||
<entity name="species" magnitude="2" volatility="5">
|
||||
<key>
|
||||
<property name="species" type="string" size="32" distinct="all" immutable="true"/>
|
||||
</key>
|
||||
</entity>
|
||||
|
||||
<entity name="breed" magnitude="1" volatility="10">
|
||||
<key>
|
||||
<property name="breed" type="string" size="32" distinct="all" immutable="true"/>
|
||||
</key>
|
||||
<property name="species" type="entity" entity="species"/>
|
||||
</entity>
|
||||
|
||||
<entity name="event" magnitude="9" volatility="2">
|
||||
<key>
|
||||
<property required="true" type="integer" name="id"
|
||||
column="id" immutable="true" distinct="system">
|
||||
<generator action="native"/>
|
||||
</property>
|
||||
</key>
|
||||
<property name="type" type="entity" entity="event-type"></property>
|
||||
<property name="actor" type="entity" entity="user" required="true" immutable="true">
|
||||
<documentation>The user who recorded this event.</documentation>
|
||||
</property>
|
||||
<promperty name="date" type="timestamp" required="true" default="now()"/>
|
||||
<property name="pastures" type="link" entity="pasture">
|
||||
<documentation>Pastures affected by this event.</documentation>
|
||||
</property>
|
||||
<property name="animals" type="link" entity="animal">
|
||||
<documentation>Animals affected by this event.</documentation>
|
||||
</property>
|
||||
<property name="summary" type="string" size="80"/>
|
||||
<property name="notes" type="text"/>
|
||||
</entity>
|
||||
|
||||
<entity name="user" magnitude="6" volatility="3">
|
||||
<key>
|
||||
<property required="true" type="string" name="username"
|
||||
column="username" immutable="true" distinct="all"/>
|
||||
</key>
|
||||
<property required="true" type="string" name="fullname"
|
||||
column="fullname" size="64" distinct="user">
|
||||
<prompt prompt="fullname" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property name="avatar" type="image" size="256">
|
||||
<documentation>An image of the canvasser, so that other members of their
|
||||
team can recognise them.</documentation>
|
||||
<prompt prompt="Avatar" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property name="bio" type="text">
|
||||
<documentation>Information the canvasser supplies about themselves; an introduction.</documentation>
|
||||
<prompt prompt="Bio" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property type="string" name="phone" column="phone" size="16"
|
||||
distinct="user">
|
||||
<documentation>Does a canvasser record need a phone field? There is a phone
|
||||
field on the elector record. I suppose, for under-16 canvassers, there may
|
||||
be no elector record.</documentation>
|
||||
<prompt prompt="phone" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property type="string" name="email" column="email" size="128"
|
||||
distinct="user">
|
||||
<prompt prompt="email" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property required="true" type="entity" name="authority_id"
|
||||
entity="authorities" farkey="id">
|
||||
<prompt prompt="authority_id" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
<property type="boolean" name="authorised" column="authorised">
|
||||
<prompt prompt="authorised" locale="en_GB.UTF-8"/>
|
||||
</property>
|
||||
</entity>
|
||||
</application>
|
149
project.clj
Normal file
149
project.clj
Normal file
|
@ -0,0 +1,149 @@
|
|||
(defproject pastoralist "0.1.0-SNAPSHOT"
|
||||
|
||||
:description "FIXME: write description"
|
||||
:url "http://example.com/FIXME"
|
||||
|
||||
:dependencies [[adl-support "0.1.6"]
|
||||
[ch.qos.logback/logback-classic "1.2.3"]
|
||||
[cheshire "5.9.0"]
|
||||
[clj-oauth "1.5.5"]
|
||||
[cljs-ajax "0.8.0"]
|
||||
[clojure.java-time "0.3.2"]
|
||||
[com.cognitect/transit-clj "0.8.319"]
|
||||
[conman "0.8.4"]
|
||||
[cprop "0.1.14"]
|
||||
[day8.re-frame/http-fx "0.1.6"]
|
||||
[expound "0.8.1"]
|
||||
[funcool/struct "1.4.0"]
|
||||
[kee-frame "0.3.3" :exclusions [metosin/reitit-core org.clojure/core.async]]
|
||||
[luminus-migrations "0.6.6"]
|
||||
[luminus-transit "0.1.2"]
|
||||
[luminus/ring-ttl-session "0.3.3"]
|
||||
[markdown-clj "1.10.0"]
|
||||
[metosin/muuntaja "0.6.6"]
|
||||
[metosin/reitit "0.3.10"]
|
||||
[metosin/ring-http-response "0.9.1"]
|
||||
[mount "0.1.16"]
|
||||
[nrepl "0.6.0"]
|
||||
[org.clojure/clojure "1.10.1"]
|
||||
[org.clojure/clojurescript "1.10.597" :scope "provided"]
|
||||
[org.clojure/tools.cli "0.4.2"]
|
||||
[org.clojure/tools.logging "0.5.0"]
|
||||
[org.postgresql/postgresql "42.2.8"]
|
||||
[org.webjars.npm/bulma "0.8.0"]
|
||||
[org.webjars.npm/material-icons "0.3.1"]
|
||||
[org.webjars/webjars-locator "0.38"]
|
||||
[re-frame "0.10.9"]
|
||||
[reagent "0.9.0-rc3"]
|
||||
[ring-webjars "0.2.0"]
|
||||
[ring/ring-core "1.8.0"]
|
||||
[ring/ring-defaults "0.3.2"]
|
||||
[ring/ring-servlet "1.7.1"]
|
||||
[selmer "1.12.17"]]
|
||||
|
||||
:min-lein-version "2.0.0"
|
||||
|
||||
:source-paths ["src/clj" "src/cljs" "src/cljc"]
|
||||
:test-paths ["test/clj"]
|
||||
:resource-paths ["resources" "target/cljsbuild"]
|
||||
:target-path "target/%s/"
|
||||
:main ^:skip-aot pastoralist.core
|
||||
|
||||
:plugins [[lein-adl "0.1.8-SNAPSHOT"]
|
||||
[lein-cljsbuild "1.1.7"]
|
||||
[lein-kibit "0.1.2"]
|
||||
[lein-uberwar "0.2.0"]]
|
||||
:uberwar
|
||||
{:handler pastoralist.handler/app
|
||||
:init pastoralist.handler/init
|
||||
:destroy pastoralist.handler/destroy
|
||||
:name "pastoralist.war"}
|
||||
|
||||
:clean-targets ^{:protect false}
|
||||
[:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]]
|
||||
:figwheel
|
||||
{:http-server-root "public"
|
||||
:server-logfile "log/figwheel-logfile.log"
|
||||
:nrepl-port 7002
|
||||
:css-dirs ["resources/public/css"]
|
||||
:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
|
||||
|
||||
|
||||
:profiles
|
||||
{:uberjar {:omit-source true
|
||||
:prep-tasks ["compile" ["cljsbuild" "once" "min"]]
|
||||
:cljsbuild{:builds
|
||||
{:min
|
||||
{:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"]
|
||||
:compiler
|
||||
{:output-dir "target/cljsbuild/public/js"
|
||||
:output-to "target/cljsbuild/public/js/app.js"
|
||||
:source-map "target/cljsbuild/public/js/app.js.map"
|
||||
:optimizations :advanced
|
||||
:pretty-print false
|
||||
:infer-externs true
|
||||
:closure-warnings
|
||||
{:externs-validation :off :non-standard-jsdoc :off}
|
||||
:externs ["react/externs/react.js"]}}}}
|
||||
|
||||
:aot :all
|
||||
:uberjar-name "pastoralist.jar"
|
||||
:source-paths ["env/prod/clj"]
|
||||
:resource-paths ["env/prod/resources"]}
|
||||
|
||||
:dev [:project/dev :profiles/dev]
|
||||
:test [:project/dev :project/test :profiles/test]
|
||||
|
||||
:project/dev {:jvm-opts ["-Dconf=dev-config.edn"]
|
||||
:dependencies [[binaryage/devtools "0.9.10"]
|
||||
[cider/piggieback "0.4.2"]
|
||||
[directory-naming/naming-java "0.8"]
|
||||
[doo "0.1.11"]
|
||||
[figwheel-sidecar "0.5.19"]
|
||||
[luminus-jetty "0.1.7"]
|
||||
[pjstadig/humane-test-output "0.10.0"]
|
||||
[prone "2019-07-08"]
|
||||
[re-frisk "0.5.4.1"]
|
||||
[ring/ring-devel "1.8.0"]
|
||||
[ring/ring-mock "0.4.0"]]
|
||||
:plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
|
||||
[jonase/eastwood "0.3.5"]
|
||||
[lein-doo "0.1.11"]
|
||||
[lein-figwheel "0.5.19"]]
|
||||
:cljsbuild{:builds
|
||||
{:app
|
||||
{:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"]
|
||||
:figwheel {:on-jsload "pastoralist.core/mount-components"}
|
||||
:compiler
|
||||
{:output-dir "target/cljsbuild/public/js/out"
|
||||
:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}
|
||||
:optimizations :none
|
||||
:preloads [re-frisk.preload]
|
||||
:output-to "target/cljsbuild/public/js/app.js"
|
||||
:asset-path "/js/out"
|
||||
:source-map true
|
||||
:main "pastoralist.app"
|
||||
:pretty-print true}}}}
|
||||
|
||||
|
||||
:doo {:build "test"}
|
||||
:source-paths ["env/dev/clj"]
|
||||
:resource-paths ["env/dev/resources"]
|
||||
:repl-options {:init-ns user}
|
||||
:injections [(require 'pjstadig.humane-test-output)
|
||||
(pjstadig.humane-test-output/activate!)]}
|
||||
:project/test {:jvm-opts ["-Dconf=test-config.edn"]
|
||||
:resource-paths ["env/test/resources"]
|
||||
:cljsbuild
|
||||
{:builds
|
||||
{:test
|
||||
{:source-paths ["src/cljc" "src/cljs" "test/cljs"]
|
||||
:compiler
|
||||
{:output-to "target/test.js"
|
||||
:main "pastoralist.doo-runner"
|
||||
:optimizations :whitespace
|
||||
:pretty-print true}}}}
|
||||
|
||||
}
|
||||
:profiles/dev {}
|
||||
:profiles/test {}})
|
103
resources/docs/docs.md
Normal file
103
resources/docs/docs.md
Normal file
|
@ -0,0 +1,103 @@
|
|||
<h2 class="alert alert-success">Congratulations, your <a class="alert-link" href="http://luminusweb.net">Luminus</a> site is ready!</h2>
|
||||
|
||||
This page will help guide you through the first steps of building your site.
|
||||
|
||||
#### Why are you seeing this page?
|
||||
|
||||
The `home-routes` handler in the `pastoralist.routes.home` namespace
|
||||
defines the route that invokes the `home-page` function whenever an HTTP
|
||||
request is made to the `/` URI using the `GET` method.
|
||||
|
||||
```
|
||||
(defn home-routes []
|
||||
[""
|
||||
{:middleware [middleware/wrap-csrf
|
||||
middleware/wrap-formats]}
|
||||
["/" {:get home-page}]
|
||||
["/docs" {:get (fn [_]
|
||||
(-> (response/ok (-> "docs/docs.md" io/resource slurp))
|
||||
(response/header "Content-Type" "text/plain; charset=utf-8")))}]])
|
||||
```
|
||||
|
||||
The `home-page` function will in turn call the `pastoralist.layout/render` function
|
||||
to render the HTML content:
|
||||
|
||||
```
|
||||
(defn home-page [_]
|
||||
(layout/render "home.html"))
|
||||
```
|
||||
|
||||
The page contains a link to the compiled ClojureScript found in the `target/cljsbuild/public` folder:
|
||||
|
||||
```
|
||||
{% script "/js/app.js" %}
|
||||
```
|
||||
|
||||
The rest of this page is rendered by ClojureScript found in the `src/cljs/pastoralist/core.cljs` file.
|
||||
|
||||
|
||||
|
||||
#### Organizing the routes
|
||||
|
||||
The routes are aggregated and wrapped with middleware in the `pastoralist.handler` namespace:
|
||||
|
||||
```
|
||||
(mount/defstate app
|
||||
:start
|
||||
(middleware/wrap-base
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
[(home-routes)])
|
||||
(ring/routes
|
||||
(ring/create-resource-handler
|
||||
{:path "/"})
|
||||
(wrap-content-type
|
||||
(wrap-webjars (constantly nil)))
|
||||
(ring/create-default-handler
|
||||
{:not-found
|
||||
(constantly (error-page {:status 404, :title "404 - Page not found"}))
|
||||
:method-not-allowed
|
||||
(constantly (error-page {:status 405, :title "405 - Not allowed"}))
|
||||
:not-acceptable
|
||||
(constantly (error-page {:status 406, :title "406 - Not acceptable"}))})))))
|
||||
```
|
||||
|
||||
The `app` definition groups all the routes in the application into a single handler.
|
||||
A default route group is added to handle the `404`, `405`, and `406` errors.
|
||||
|
||||
<a class="btn btn-primary" href="https://metosin.github.io/reitit/basics">learn more about routing »</a>
|
||||
|
||||
#### Managing your middleware
|
||||
|
||||
Request middleware functions are located under the `pastoralist.middleware` namespace.
|
||||
|
||||
This namespace is reserved for any custom middleware for the application. Some default middleware is
|
||||
already defined here. The middleware is assembled in the `wrap-base` function.
|
||||
|
||||
Middleware used for development is placed in the `pastoralist.dev-middleware` namespace found in
|
||||
the `env/dev/clj/` source path.
|
||||
|
||||
<a class="btn btn-primary" href="http://www.luminusweb.net/docs/middleware.md">learn more about middleware »</a>
|
||||
|
||||
<div class="bs-callout bs-callout-danger">
|
||||
|
||||
#### Database configuration is required
|
||||
|
||||
If you haven't already, then please follow the steps below to configure your database connection and run the necessary migrations.
|
||||
|
||||
* Create the database for your application.
|
||||
* Update the connection URL in the `dev-config.edn` and `test-config.edn` files with your database name and login credentials.
|
||||
* Run `lein run migrate` in the root of the project to create the tables.
|
||||
* Let `mount` know to start the database connection by `require`-ing `pastoralist.db.core` in some other namespace.
|
||||
* Restart the application.
|
||||
|
||||
<a class="btn btn-primary" href="http://www.luminusweb.net/docs/database.md">learn more about database access »</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
#### Need some help?
|
||||
|
||||
Visit the [official documentation](http://www.luminusweb.net/docs) for examples
|
||||
on how to accomplish common tasks with Luminus. The `#luminus` channel on the [Clojurians Slack](http://clojurians.net/) and [Google Group](https://groups.google.com/forum/#!forum/luminusweb) are both great places to seek help and discuss projects with other users.
|
49
resources/html/error.html
Normal file
49
resources/html/error.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Something Bad Happened</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% style "/assets/bulma/css/bulma.min.css" %}
|
||||
<style type="text/css">
|
||||
html {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
html body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
html .container-fluid {
|
||||
display: table;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
html .row-fluid {
|
||||
display: table-cell;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container is-fluid">
|
||||
<div class="has-text-centered">
|
||||
<h1><span class="is-size-4 has-text-danger">Error: {{status}}</span></h1>
|
||||
<hr>
|
||||
{% if title %}
|
||||
<h2 class="without-margin">{{title}}</h2>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
<h4 class="text-danger">{{message}}</h4>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
41
resources/html/home.html
Normal file
41
resources/html/home.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Welcome to pastoralist</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<div class="content">
|
||||
<h4 class="title">Welcome to pastoralist</h4>
|
||||
<p>If you're seeing this message, that means you haven't yet compiled your ClojureScript!</p>
|
||||
<p>Please run <code>lein figwheel</code> to start the ClojureScript compiler and reload the page.</p>
|
||||
<h4>For better ClojureScript development experience in Chrome follow these steps:</h4>
|
||||
<ul>
|
||||
<li>Open DevTools
|
||||
<li>Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console)
|
||||
<li>Check-in "Enable custom formatters"
|
||||
<li>Close DevTools
|
||||
<li>Open DevTools
|
||||
</ul>
|
||||
<p>See <a href="http://www.luminusweb.net/docs/clojurescript.md">ClojureScript</a> documentation for further details.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- scripts and styles -->
|
||||
{% style "/assets/bulma/css/bulma.min.css" %}
|
||||
{% style "/assets/material-icons/css/material-icons.min.css" %}
|
||||
{% style "/css/screen.css" %}
|
||||
|
||||
<script type="text/javascript">
|
||||
var csrfToken = "{{csrf-token}}";
|
||||
</script>
|
||||
{% script "/js/app.js" %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE users;
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE users
|
||||
(id VARCHAR(20) PRIMARY KEY,
|
||||
first_name VARCHAR(30),
|
||||
last_name VARCHAR(30),
|
||||
email VARCHAR(30),
|
||||
admin BOOLEAN,
|
||||
last_login TIMESTAMP,
|
||||
is_active BOOLEAN,
|
||||
pass VARCHAR(300));
|
35
resources/public/css/screen.css
Normal file
35
resources/public/css/screen.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
html,
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/assets/material-icons/iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(/assets/material-icons/iconfont/MaterialIcons-Regular.woff2) format('woff2'),
|
||||
url(/assets/material-icons/iconfont/MaterialIcons-Regular.woff) format('woff'),
|
||||
url(/assets/material-icons/iconfont/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
BIN
resources/public/favicon.ico
Normal file
BIN
resources/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/public/img/warning_clojure.png
Normal file
BIN
resources/public/img/warning_clojure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
21
resources/sql/queries.sql
Normal file
21
resources/sql/queries.sql
Normal file
|
@ -0,0 +1,21 @@
|
|||
-- :name create-user! :! :n
|
||||
-- :doc creates a new user record
|
||||
INSERT INTO users
|
||||
(id, first_name, last_name, email, pass)
|
||||
VALUES (:id, :first_name, :last_name, :email, :pass)
|
||||
|
||||
-- :name update-user! :! :n
|
||||
-- :doc updates an existing user record
|
||||
UPDATE users
|
||||
SET first_name = :first_name, last_name = :last_name, email = :email
|
||||
WHERE id = :id
|
||||
|
||||
-- :name get-user :? :1
|
||||
-- :doc retrieves a user record given the id
|
||||
SELECT * FROM users
|
||||
WHERE id = :id
|
||||
|
||||
-- :name delete-user! :! :n
|
||||
-- :doc deletes a user record given the id
|
||||
DELETE FROM users
|
||||
WHERE id = :id
|
13
src/clj/pastoralist/config.clj
Normal file
13
src/clj/pastoralist/config.clj
Normal file
|
@ -0,0 +1,13 @@
|
|||
(ns pastoralist.config
|
||||
(:require
|
||||
[cprop.core :refer [load-config]]
|
||||
[cprop.source :as source]
|
||||
[mount.core :refer [args defstate]]))
|
||||
|
||||
(defstate env
|
||||
:start
|
||||
(load-config
|
||||
:merge
|
||||
[(args)
|
||||
(source/from-system-props)
|
||||
(source/from-env)]))
|
86
src/clj/pastoralist/db/core.clj
Normal file
86
src/clj/pastoralist/db/core.clj
Normal file
|
@ -0,0 +1,86 @@
|
|||
(ns pastoralist.db.core
|
||||
(:require
|
||||
[cheshire.core :refer [generate-string parse-string]]
|
||||
[clojure.java.jdbc :as jdbc]
|
||||
[clojure.tools.logging :as log]
|
||||
[conman.core :as conman]
|
||||
[java-time :as jt]
|
||||
[java-time.pre-java8]
|
||||
[pastoralist.config :refer [env]]
|
||||
[mount.core :refer [defstate]])
|
||||
(:import org.postgresql.util.PGobject
|
||||
java.sql.Array
|
||||
clojure.lang.IPersistentMap
|
||||
clojure.lang.IPersistentVector
|
||||
[java.sql
|
||||
BatchUpdateException
|
||||
PreparedStatement]))
|
||||
(defstate ^:dynamic *db*
|
||||
:start (if-let [jdbc-url (env :database-url)]
|
||||
(conman/connect! {:jdbc-url jdbc-url})
|
||||
(do
|
||||
(log/warn "database connection URL was not found, please set :database-url in your config, e.g: dev-config.edn")
|
||||
*db*))
|
||||
:stop (conman/disconnect! *db*))
|
||||
|
||||
(conman/bind-connection *db* "sql/queries.sql")
|
||||
|
||||
|
||||
(extend-protocol jdbc/IResultSetReadColumn
|
||||
java.sql.Timestamp
|
||||
(result-set-read-column [v _2 _3]
|
||||
(.toLocalDateTime v))
|
||||
java.sql.Date
|
||||
(result-set-read-column [v _2 _3]
|
||||
(.toLocalDate v))
|
||||
java.sql.Time
|
||||
(result-set-read-column [v _2 _3]
|
||||
(.toLocalTime v))
|
||||
Array
|
||||
(result-set-read-column [v _ _] (vec (.getArray v)))
|
||||
PGobject
|
||||
(result-set-read-column [pgobj _metadata _index]
|
||||
(let [type (.getType pgobj)
|
||||
value (.getValue pgobj)]
|
||||
(case type
|
||||
"json" (parse-string value true)
|
||||
"jsonb" (parse-string value true)
|
||||
"citext" (str value)
|
||||
value))))
|
||||
|
||||
(defn to-pg-json [value]
|
||||
(doto (PGobject.)
|
||||
(.setType "jsonb")
|
||||
(.setValue (generate-string value))))
|
||||
|
||||
(extend-type clojure.lang.IPersistentVector
|
||||
jdbc/ISQLParameter
|
||||
(set-parameter [v ^java.sql.PreparedStatement stmt ^long idx]
|
||||
(let [conn (.getConnection stmt)
|
||||
meta (.getParameterMetaData stmt)
|
||||
type-name (.getParameterTypeName meta idx)]
|
||||
(if-let [elem-type (when (= (first type-name) \_) (apply str (rest type-name)))]
|
||||
(.setObject stmt idx (.createArrayOf conn elem-type (to-array v)))
|
||||
(.setObject stmt idx (to-pg-json v))))))
|
||||
|
||||
(extend-protocol jdbc/ISQLValue
|
||||
java.util.Date
|
||||
(sql-value [v]
|
||||
(java.sql.Timestamp. (.getTime v)))
|
||||
java.time.LocalTime
|
||||
(sql-value [v]
|
||||
(jt/sql-time v))
|
||||
java.time.LocalDate
|
||||
(sql-value [v]
|
||||
(jt/sql-date v))
|
||||
java.time.LocalDateTime
|
||||
(sql-value [v]
|
||||
(jt/sql-timestamp v))
|
||||
java.time.ZonedDateTime
|
||||
(sql-value [v]
|
||||
(jt/sql-timestamp v))
|
||||
IPersistentMap
|
||||
(sql-value [value] (to-pg-json value))
|
||||
IPersistentVector
|
||||
(sql-value [value] (to-pg-json value)))
|
||||
|
64
src/clj/pastoralist/handler.clj
Normal file
64
src/clj/pastoralist/handler.clj
Normal file
|
@ -0,0 +1,64 @@
|
|||
(ns pastoralist.handler
|
||||
(:require
|
||||
[pastoralist.middleware :as middleware]
|
||||
[pastoralist.layout :refer [error-page]]
|
||||
[pastoralist.routes.home :refer [home-routes]]
|
||||
[pastoralist.routes.services :refer [service-routes]]
|
||||
[pastoralist.routes.oauth :refer [oauth-routes]]
|
||||
[reitit.swagger-ui :as swagger-ui]
|
||||
[reitit.ring :as ring]
|
||||
[ring.middleware.content-type :refer [wrap-content-type]]
|
||||
[ring.middleware.webjars :refer [wrap-webjars]]
|
||||
[pastoralist.env :refer [defaults]]
|
||||
[mount.core :as mount]
|
||||
[clojure.tools.logging :as log]
|
||||
[pastoralist.config :refer [env]]))
|
||||
|
||||
(mount/defstate init-app
|
||||
:start ((or (:init defaults) (fn [])))
|
||||
:stop ((or (:stop defaults) (fn []))))
|
||||
|
||||
(defn init
|
||||
"init will be called once when
|
||||
app is deployed as a servlet on
|
||||
an app server such as Tomcat
|
||||
put any initialization code here"
|
||||
[]
|
||||
(doseq [component (:started (mount/start))]
|
||||
(log/info component "started")))
|
||||
|
||||
(defn destroy
|
||||
"destroy will be called when your application
|
||||
shuts down, put any clean up code here"
|
||||
[]
|
||||
(doseq [component (:stopped (mount/stop))]
|
||||
(log/info component "stopped"))
|
||||
(shutdown-agents)
|
||||
(log/info "pastoralist has shut down!"))
|
||||
|
||||
(mount/defstate app-routes
|
||||
:start
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
[(home-routes)
|
||||
(service-routes)
|
||||
(oauth-routes)])
|
||||
(ring/routes
|
||||
(swagger-ui/create-swagger-ui-handler
|
||||
{:path "/swagger-ui"
|
||||
:url "/api/swagger.json"
|
||||
:config {:validator-url nil}})
|
||||
(ring/create-resource-handler
|
||||
{:path "/"})
|
||||
(wrap-content-type
|
||||
(wrap-webjars (constantly nil)))
|
||||
(ring/create-default-handler
|
||||
{:not-found
|
||||
(constantly (error-page {:status 404, :title "404 - Page not found"}))
|
||||
:method-not-allowed
|
||||
(constantly (error-page {:status 405, :title "405 - Not allowed"}))
|
||||
:not-acceptable
|
||||
(constantly (error-page {:status 406, :title "406 - Not acceptable"}))}))))
|
||||
|
||||
(defn app []
|
||||
(middleware/wrap-base #'app-routes))
|
39
src/clj/pastoralist/layout.clj
Normal file
39
src/clj/pastoralist/layout.clj
Normal file
|
@ -0,0 +1,39 @@
|
|||
(ns pastoralist.layout
|
||||
(:require
|
||||
[clojure.java.io]
|
||||
[selmer.parser :as parser]
|
||||
[selmer.filters :as filters]
|
||||
[markdown.core :refer [md-to-html-string]]
|
||||
[ring.util.http-response :refer [content-type ok]]
|
||||
[ring.util.anti-forgery :refer [anti-forgery-field]]
|
||||
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
|
||||
[ring.util.response]))
|
||||
|
||||
(parser/set-resource-path! (clojure.java.io/resource "html"))
|
||||
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
|
||||
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
|
||||
|
||||
(defn render
|
||||
"renders the HTML template located relative to resources/html"
|
||||
[request template & [params]]
|
||||
(content-type
|
||||
(ok
|
||||
(parser/render-file
|
||||
template
|
||||
(assoc params
|
||||
:page template
|
||||
:csrf-token *anti-forgery-token*)))
|
||||
"text/html; charset=utf-8"))
|
||||
|
||||
(defn error-page
|
||||
"error-details should be a map containing the following keys:
|
||||
:status - error status
|
||||
:title - error title (optional)
|
||||
:message - detailed error message (optional)
|
||||
|
||||
returns a response map with the error page as the body
|
||||
and the status specified by the status key"
|
||||
[error-details]
|
||||
{:status (:status error-details)
|
||||
:headers {"Content-Type" "text/html; charset=utf-8"}
|
||||
:body (parser/render-file "error.html" error-details)})
|
49
src/clj/pastoralist/middleware.clj
Normal file
49
src/clj/pastoralist/middleware.clj
Normal file
|
@ -0,0 +1,49 @@
|
|||
(ns pastoralist.middleware
|
||||
(:require
|
||||
[pastoralist.env :refer [defaults]]
|
||||
[cheshire.generate :as cheshire]
|
||||
[cognitect.transit :as transit]
|
||||
[clojure.tools.logging :as log]
|
||||
[pastoralist.layout :refer [error-page]]
|
||||
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
|
||||
[pastoralist.middleware.formats :as formats]
|
||||
[muuntaja.middleware :refer [wrap-format wrap-params]]
|
||||
[pastoralist.config :refer [env]]
|
||||
[ring-ttl-session.core :refer [ttl-memory-store]]
|
||||
[ring.middleware.defaults :refer [site-defaults wrap-defaults]])
|
||||
|
||||
)
|
||||
|
||||
(defn wrap-internal-error [handler]
|
||||
(fn [req]
|
||||
(try
|
||||
(handler req)
|
||||
(catch Throwable t
|
||||
(log/error t (.getMessage t))
|
||||
(error-page {:status 500
|
||||
:title "Something very bad has happened!"
|
||||
:message "We've dispatched a team of highly trained gnomes to take care of the problem."})))))
|
||||
|
||||
(defn wrap-csrf [handler]
|
||||
(wrap-anti-forgery
|
||||
handler
|
||||
{:error-response
|
||||
(error-page
|
||||
{:status 403
|
||||
:title "Invalid anti-forgery token"})}))
|
||||
|
||||
|
||||
(defn wrap-formats [handler]
|
||||
(let [wrapped (-> handler wrap-params (wrap-format formats/instance))]
|
||||
(fn [request]
|
||||
;; disable wrap-formats for websockets
|
||||
;; since they're not compatible with this middleware
|
||||
((if (:websocket? request) handler wrapped) request))))
|
||||
|
||||
(defn wrap-base [handler]
|
||||
(-> ((:middleware defaults) handler)
|
||||
(wrap-defaults
|
||||
(-> site-defaults
|
||||
(assoc-in [:security :anti-forgery] false)
|
||||
(assoc-in [:session :store] (ttl-memory-store (* 60 30)))))
|
||||
wrap-internal-error))
|
24
src/clj/pastoralist/middleware/exception.clj
Normal file
24
src/clj/pastoralist/middleware/exception.clj
Normal file
|
@ -0,0 +1,24 @@
|
|||
(ns pastoralist.middleware.exception
|
||||
(:require [clojure.tools.logging :as log]
|
||||
[expound.alpha :as expound]
|
||||
[reitit.coercion :as coercion]
|
||||
[reitit.ring.middleware.exception :as exception]))
|
||||
|
||||
(defn coercion-error-handler [status]
|
||||
(let [printer (expound/custom-printer {:print-specs? false})]
|
||||
(fn [exception request]
|
||||
{:status status
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (with-out-str (printer (-> exception ex-data :problems)))})))
|
||||
|
||||
(def exception-middleware
|
||||
(exception/create-exception-middleware
|
||||
(merge
|
||||
exception/default-handlers
|
||||
{;; log stack-traces for all exceptions
|
||||
::exception/wrap (fn [handler e request]
|
||||
(log/error e (.getMessage e))
|
||||
(handler e request))
|
||||
;; human-optimized validation messages
|
||||
::coercion/request-coercion (coercion-error-handler 400)
|
||||
::coercion/response-coercion (coercion-error-handler 500)})))
|
15
src/clj/pastoralist/middleware/formats.clj
Normal file
15
src/clj/pastoralist/middleware/formats.clj
Normal file
|
@ -0,0 +1,15 @@
|
|||
(ns pastoralist.middleware.formats
|
||||
(:require
|
||||
[cognitect.transit :as transit]
|
||||
[luminus-transit.time :as time]
|
||||
[muuntaja.core :as m]))
|
||||
|
||||
(def instance
|
||||
(m/create
|
||||
(-> m/default-options
|
||||
(update-in
|
||||
[:formats "application/transit+json" :decoder-opts]
|
||||
(partial merge time/time-deserialization-handlers))
|
||||
(update-in
|
||||
[:formats "application/transit+json" :encoder-opts]
|
||||
(partial merge time/time-serialization-handlers)))))
|
27
src/clj/pastoralist/nrepl.clj
Normal file
27
src/clj/pastoralist/nrepl.clj
Normal file
|
@ -0,0 +1,27 @@
|
|||
(ns pastoralist.nrepl
|
||||
(:require
|
||||
[nrepl.server :as nrepl]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defn start
|
||||
"Start a network repl for debugging on specified port followed by
|
||||
an optional parameters map. The :bind, :transport-fn, :handler,
|
||||
:ack-port and :greeting-fn will be forwarded to
|
||||
clojure.tools.nrepl.server/start-server as they are."
|
||||
[{:keys [port bind transport-fn handler ack-port greeting-fn]}]
|
||||
(try
|
||||
(log/info "starting nREPL server on port" port)
|
||||
(nrepl/start-server :port port
|
||||
:bind bind
|
||||
:transport-fn transport-fn
|
||||
:handler handler
|
||||
:ack-port ack-port
|
||||
:greeting-fn greeting-fn)
|
||||
|
||||
(catch Throwable t
|
||||
(log/error t "failed to start nREPL")
|
||||
(throw t))))
|
||||
|
||||
(defn stop [server]
|
||||
(nrepl/stop-server server)
|
||||
(log/info "nREPL server stopped"))
|
36
src/clj/pastoralist/oauth.clj
Normal file
36
src/clj/pastoralist/oauth.clj
Normal file
|
@ -0,0 +1,36 @@
|
|||
(ns pastoralist.oauth
|
||||
(:require
|
||||
[pastoralist.config :refer [env]]
|
||||
[oauth.client :as oauth]
|
||||
[mount.core :refer [defstate]]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defstate consumer
|
||||
:start (oauth/make-consumer
|
||||
(env :oauth-consumer-key)
|
||||
(env :oauth-consumer-secret)
|
||||
(env :request-token-uri)
|
||||
(env :access-token-uri)
|
||||
(env :authorize-uri)
|
||||
:hmac-sha1))
|
||||
|
||||
(defn oauth-callback-uri
|
||||
"Generates the oauth request callback URI"
|
||||
[{:keys [headers]}]
|
||||
(str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/oauth-callback"))
|
||||
|
||||
(defn fetch-request-token
|
||||
"Fetches a request token."
|
||||
[request]
|
||||
(let [callback-uri (oauth-callback-uri request)]
|
||||
(log/info "Fetching request token using callback-uri" callback-uri)
|
||||
(oauth/request-token consumer (oauth-callback-uri request))))
|
||||
|
||||
(defn fetch-access-token
|
||||
[request_token]
|
||||
(oauth/access-token consumer request_token (:oauth_verifier request_token)))
|
||||
|
||||
(defn auth-redirect-uri
|
||||
"Gets the URI the user should be redirected to when authenticating."
|
||||
[request-token]
|
||||
(str (oauth/user-approval-uri consumer request-token)))
|
21
src/clj/pastoralist/routes/home.clj
Normal file
21
src/clj/pastoralist/routes/home.clj
Normal file
|
@ -0,0 +1,21 @@
|
|||
(ns pastoralist.routes.home
|
||||
(:require
|
||||
[pastoralist.layout :as layout]
|
||||
[pastoralist.db.core :as db]
|
||||
[clojure.java.io :as io]
|
||||
[pastoralist.middleware :as middleware]
|
||||
[ring.util.response]
|
||||
[ring.util.http-response :as response]))
|
||||
|
||||
(defn home-page [request]
|
||||
(layout/render request "home.html"))
|
||||
|
||||
(defn home-routes []
|
||||
[""
|
||||
{:middleware [middleware/wrap-csrf
|
||||
middleware/wrap-formats]}
|
||||
["/" {:get home-page}]
|
||||
["/docs" {:get (fn [_]
|
||||
(-> (response/ok (-> "docs/docs.md" io/resource slurp))
|
||||
(response/header "Content-Type" "text/plain; charset=utf-8")))}]])
|
||||
|
33
src/clj/pastoralist/routes/oauth.clj
Normal file
33
src/clj/pastoralist/routes/oauth.clj
Normal file
|
@ -0,0 +1,33 @@
|
|||
(ns pastoralist.routes.oauth
|
||||
(:require
|
||||
[ring.util.http-response :refer [ok found]]
|
||||
[clojure.java.io :as io]
|
||||
[pastoralist.oauth :as oauth]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defn oauth-init
|
||||
"Initiates the Twitter OAuth"
|
||||
[request]
|
||||
(-> (oauth/fetch-request-token request)
|
||||
:oauth_token
|
||||
oauth/auth-redirect-uri
|
||||
found))
|
||||
|
||||
(defn oauth-callback
|
||||
"Handles the callback from Twitter."
|
||||
[{:keys [session params]}]
|
||||
; oauth request was denied by user
|
||||
(if (:denied params)
|
||||
(-> (found "/")
|
||||
(assoc :flash {:denied true}))
|
||||
; fetch the request token and do anything else you wanna do if not denied.
|
||||
(let [{:keys [user_id screen_name]} (oauth/fetch-access-token params)]
|
||||
(log/info "successfully authenticated as" user_id screen_name)
|
||||
(-> (found "/")
|
||||
(assoc :session
|
||||
(assoc session :user-id user_id :screen-name screen_name))))))
|
||||
|
||||
(defn oauth-routes []
|
||||
["/oauth"
|
||||
["/oauth-init" {:get oauth-init}]
|
||||
["/oauth-callback" {:get oauth-callback}]])
|
91
src/clj/pastoralist/routes/services.clj
Normal file
91
src/clj/pastoralist/routes/services.clj
Normal file
|
@ -0,0 +1,91 @@
|
|||
(ns pastoralist.routes.services
|
||||
(:require
|
||||
[reitit.swagger :as swagger]
|
||||
[reitit.swagger-ui :as swagger-ui]
|
||||
[reitit.ring.coercion :as coercion]
|
||||
[reitit.coercion.spec :as spec-coercion]
|
||||
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||
[reitit.ring.middleware.multipart :as multipart]
|
||||
[reitit.ring.middleware.parameters :as parameters]
|
||||
[pastoralist.middleware.formats :as formats]
|
||||
[pastoralist.middleware.exception :as exception]
|
||||
[ring.util.http-response :refer :all]
|
||||
[clojure.java.io :as io]))
|
||||
|
||||
(defn service-routes []
|
||||
["/api"
|
||||
{:coercion spec-coercion/coercion
|
||||
:muuntaja formats/instance
|
||||
:swagger {:id ::api}
|
||||
:middleware [;; query-params & form-params
|
||||
parameters/parameters-middleware
|
||||
;; content-negotiation
|
||||
muuntaja/format-negotiate-middleware
|
||||
;; encoding response body
|
||||
muuntaja/format-response-middleware
|
||||
;; exception handling
|
||||
exception/exception-middleware
|
||||
;; decoding request body
|
||||
muuntaja/format-request-middleware
|
||||
;; coercing response bodys
|
||||
coercion/coerce-response-middleware
|
||||
;; coercing request parameters
|
||||
coercion/coerce-request-middleware
|
||||
;; multipart
|
||||
multipart/multipart-middleware]}
|
||||
|
||||
;; swagger documentation
|
||||
["" {:no-doc true
|
||||
:swagger {:info {:title "my-api"
|
||||
:description "https://cljdoc.org/d/metosin/reitit"}}}
|
||||
|
||||
["/swagger.json"
|
||||
{:get (swagger/create-swagger-handler)}]
|
||||
|
||||
["/api-docs/*"
|
||||
{:get (swagger-ui/create-swagger-ui-handler
|
||||
{:url "/api/swagger.json"
|
||||
:config {:validator-url nil}})}]]
|
||||
|
||||
["/ping"
|
||||
{:get (constantly (ok {:message "pong"}))}]
|
||||
|
||||
|
||||
["/math"
|
||||
{:swagger {:tags ["math"]}}
|
||||
|
||||
["/plus"
|
||||
{:get {:summary "plus with spec query parameters"
|
||||
:parameters {:query {:x int?, :y int?}}
|
||||
:responses {200 {:body {:total pos-int?}}}
|
||||
:handler (fn [{{{:keys [x y]} :query} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}
|
||||
:post {:summary "plus with spec body parameters"
|
||||
:parameters {:body {:x int?, :y int?}}
|
||||
:responses {200 {:body {:total pos-int?}}}
|
||||
:handler (fn [{{{:keys [x y]} :body} :parameters}]
|
||||
{:status 200
|
||||
:body {:total (+ x y)}})}}]]
|
||||
|
||||
["/files"
|
||||
{:swagger {:tags ["files"]}}
|
||||
|
||||
["/upload"
|
||||
{:post {:summary "upload a file"
|
||||
:parameters {:multipart {:file multipart/temp-file-part}}
|
||||
:responses {200 {:body {:name string?, :size int?}}}
|
||||
:handler (fn [{{{:keys [file]} :multipart} :parameters}]
|
||||
{:status 200
|
||||
:body {:name (:filename file)
|
||||
:size (:size file)}})}}]
|
||||
|
||||
["/download"
|
||||
{:get {:summary "downloads a file"
|
||||
:swagger {:produces ["image/png"]}
|
||||
:handler (fn [_]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "image/png"}
|
||||
:body (-> "public/img/warning_clojure.png"
|
||||
(io/resource)
|
||||
(io/input-stream))})}}]]])
|
2
src/cljc/pastoralist/validation.cljc
Normal file
2
src/cljc/pastoralist/validation.cljc
Normal file
|
@ -0,0 +1,2 @@
|
|||
(ns pastoralist.validation
|
||||
(:require [struct.core :as st]))
|
30
src/cljs/pastoralist/ajax.cljs
Normal file
30
src/cljs/pastoralist/ajax.cljs
Normal file
|
@ -0,0 +1,30 @@
|
|||
(ns pastoralist.ajax
|
||||
(:require
|
||||
[ajax.core :as ajax]
|
||||
[luminus-transit.time :as time]
|
||||
[cognitect.transit :as transit]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn local-uri? [{:keys [uri]}]
|
||||
(not (re-find #"^\w+?://" uri)))
|
||||
|
||||
(defn default-headers [request]
|
||||
(if (local-uri? request)
|
||||
(-> request
|
||||
(update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
|
||||
request))
|
||||
|
||||
;; injects transit serialization config into request options
|
||||
(defn as-transit [opts]
|
||||
(merge {:raw false
|
||||
:format :transit
|
||||
:response-format :transit
|
||||
:reader (transit/reader :json time/time-deserialization-handlers)
|
||||
:writer (transit/writer :json time/time-serialization-handlers)}
|
||||
opts))
|
||||
|
||||
(defn load-interceptors! []
|
||||
(swap! ajax/default-interceptors
|
||||
conj
|
||||
(ajax/to-interceptor {:name "default headers"
|
||||
:request default-headers})))
|
55
src/cljs/pastoralist/core.cljs
Normal file
55
src/cljs/pastoralist/core.cljs
Normal file
|
@ -0,0 +1,55 @@
|
|||
(ns pastoralist.core
|
||||
(:require
|
||||
[kee-frame.core :as kf]
|
||||
[re-frame.core :as rf]
|
||||
[ajax.core :as http]
|
||||
[pastoralist.ajax :as ajax]
|
||||
[pastoralist.routing :as routing]
|
||||
[pastoralist.view :as view]))
|
||||
|
||||
|
||||
(rf/reg-event-fx
|
||||
::load-about-page
|
||||
(constantly nil))
|
||||
|
||||
(kf/reg-controller
|
||||
::about-controller
|
||||
{:params (constantly true)
|
||||
:start [::load-about-page]})
|
||||
|
||||
(rf/reg-sub
|
||||
:docs
|
||||
(fn [db _]
|
||||
(:docs db)))
|
||||
|
||||
(kf/reg-chain
|
||||
::load-home-page
|
||||
(fn [_ _]
|
||||
{:http-xhrio {:method :get
|
||||
:uri "/docs"
|
||||
:response-format (http/raw-response-format)
|
||||
:on-failure [:common/set-error]}})
|
||||
(fn [{:keys [db]} [_ docs]]
|
||||
{:db (assoc db :docs docs)}))
|
||||
|
||||
|
||||
(kf/reg-controller
|
||||
::home-controller
|
||||
{:params (constantly true)
|
||||
:start [::load-home-page]})
|
||||
|
||||
;; -------------------------
|
||||
;; Initialize app
|
||||
(defn mount-components
|
||||
([] (mount-components true))
|
||||
([debug?]
|
||||
(rf/clear-subscription-cache!)
|
||||
(kf/start! {:debug? (boolean debug?)
|
||||
:routes routing/routes
|
||||
:hash-routing? true
|
||||
:initial-db {}
|
||||
:root-component [view/root-component]})))
|
||||
|
||||
(defn init! [debug?]
|
||||
(ajax/load-interceptors!)
|
||||
(mount-components debug?))
|
24
src/cljs/pastoralist/routing.cljs
Normal file
24
src/cljs/pastoralist/routing.cljs
Normal file
|
@ -0,0 +1,24 @@
|
|||
(ns pastoralist.routing
|
||||
(:require
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(def routes
|
||||
[["/" :home]
|
||||
["/about" :about]])
|
||||
|
||||
(rf/reg-sub
|
||||
:nav/route
|
||||
:<- [:kee-frame/route]
|
||||
identity)
|
||||
|
||||
(rf/reg-event-fx
|
||||
:nav/route-name
|
||||
(fn [_ [_ route-name]]
|
||||
{:navigate-to [route-name]}))
|
||||
|
||||
|
||||
(rf/reg-sub
|
||||
:nav/page
|
||||
:<- [:nav/route]
|
||||
(fn [route _]
|
||||
(-> route :data :name)))
|
45
src/cljs/pastoralist/view.cljs
Normal file
45
src/cljs/pastoralist/view.cljs
Normal file
|
@ -0,0 +1,45 @@
|
|||
(ns pastoralist.view
|
||||
(:require
|
||||
[kee-frame.core :as kf]
|
||||
[markdown.core :refer [md->html]]
|
||||
[reagent.core :as r]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn nav-link [title page]
|
||||
[:a.navbar-item
|
||||
{:href (kf/path-for [page])
|
||||
:class (when (= page @(rf/subscribe [:nav/page])) "is-active")}
|
||||
title])
|
||||
|
||||
(defn navbar []
|
||||
(r/with-let [expanded? (r/atom false)]
|
||||
[:nav.navbar.is-info>div.container
|
||||
[:div.navbar-brand
|
||||
[:a.navbar-item {:href "/" :style {:font-weight :bold}} "pastoralist"]
|
||||
[:span.navbar-burger.burger
|
||||
{:data-target :nav-menu
|
||||
:on-click #(swap! expanded? not)
|
||||
:class (when @expanded? :is-active)}
|
||||
[:span][:span][:span]]]
|
||||
[:div#nav-menu.navbar-menu
|
||||
{:class (when @expanded? :is-active)}
|
||||
[:div.navbar-start
|
||||
[nav-link "Home" :home]
|
||||
[nav-link "About" :about]]]]))
|
||||
|
||||
(defn about-page []
|
||||
[:section.section>div.container>div.content
|
||||
[:img {:src "/img/warning_clojure.png"}]])
|
||||
|
||||
(defn home-page []
|
||||
[:section.section>div.container>div.content
|
||||
(when-let [docs @(rf/subscribe [:docs])]
|
||||
[:div {:dangerouslySetInnerHTML {:__html (md->html docs)}}])])
|
||||
|
||||
(defn root-component []
|
||||
[:div
|
||||
[navbar]
|
||||
[kf/switch-route (fn [route] (get-in route [:data :name]))
|
||||
:home home-page
|
||||
:about about-page
|
||||
nil [:div ""]]])
|
8
test-config.edn
Normal file
8
test-config.edn
Normal file
|
@ -0,0 +1,8 @@
|
|||
;; WARNING
|
||||
;; The test-config.edn file is used for local environment variables, such as database credentials.
|
||||
;; This file is listed in .gitignore and will be excluded from version control by Git.
|
||||
|
||||
{:port 3000
|
||||
; set your test database connection URL here
|
||||
; :database-url "postgresql://localhost/pastoralist_test?user=simon&password=db_user_password_here"
|
||||
}
|
38
test/clj/pastoralist/test/db/core.clj
Normal file
38
test/clj/pastoralist/test/db/core.clj
Normal file
|
@ -0,0 +1,38 @@
|
|||
(ns pastoralist.test.db.core
|
||||
(:require
|
||||
[pastoralist.db.core :refer [*db*] :as db]
|
||||
[java-time.pre-java8]
|
||||
[luminus-migrations.core :as migrations]
|
||||
[clojure.test :refer :all]
|
||||
[clojure.java.jdbc :as jdbc]
|
||||
[pastoralist.config :refer [env]]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(use-fixtures
|
||||
:once
|
||||
(fn [f]
|
||||
(mount/start
|
||||
#'pastoralist.config/env
|
||||
#'pastoralist.db.core/*db*)
|
||||
(migrations/migrate ["migrate"] (select-keys env [:database-url]))
|
||||
(f)))
|
||||
|
||||
(deftest test-users
|
||||
(jdbc/with-db-transaction [t-conn *db*]
|
||||
(jdbc/db-set-rollback-only! t-conn)
|
||||
(is (= 1 (db/create-user!
|
||||
t-conn
|
||||
{:id "1"
|
||||
:first_name "Sam"
|
||||
:last_name "Smith"
|
||||
:email "sam.smith@example.com"
|
||||
:pass "pass"})))
|
||||
(is (= {:id "1"
|
||||
:first_name "Sam"
|
||||
:last_name "Smith"
|
||||
:email "sam.smith@example.com"
|
||||
:pass "pass"
|
||||
:admin nil
|
||||
:last_login nil
|
||||
:is_active nil}
|
||||
(db/get-user t-conn {:id "1"})))))
|
52
test/clj/pastoralist/test/handler.clj
Normal file
52
test/clj/pastoralist/test/handler.clj
Normal file
|
@ -0,0 +1,52 @@
|
|||
(ns pastoralist.test.handler
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[ring.mock.request :refer :all]
|
||||
[pastoralist.handler :refer :all]
|
||||
[pastoralist.middleware.formats :as formats]
|
||||
[muuntaja.core :as m]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(defn parse-json [body]
|
||||
(m/decode formats/instance "application/json" body))
|
||||
|
||||
(use-fixtures
|
||||
:once
|
||||
(fn [f]
|
||||
(mount/start #'pastoralist.config/env
|
||||
#'pastoralist.handler/app-routes)
|
||||
(f)))
|
||||
|
||||
(deftest test-app
|
||||
(testing "main route"
|
||||
(let [response ((app) (request :get "/"))]
|
||||
(is (= 200 (:status response)))))
|
||||
|
||||
(testing "not-found route"
|
||||
(let [response ((app) (request :get "/invalid"))]
|
||||
(is (= 404 (:status response)))))
|
||||
(testing "services"
|
||||
|
||||
(testing "success"
|
||||
(let [response ((app) (-> (request :post "/api/math/plus")
|
||||
(json-body {:x 10, :y 6})))]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= {:total 16} (m/decode-response-body response)))))
|
||||
|
||||
(testing "parameter coercion error"
|
||||
(let [response ((app) (-> (request :post "/api/math/plus")
|
||||
(json-body {:x 10, :y "invalid"})))]
|
||||
(is (= 400 (:status response)))))
|
||||
|
||||
(testing "response coercion error"
|
||||
(let [response ((app) (-> (request :post "/api/math/plus")
|
||||
(json-body {:x -10, :y 6})))]
|
||||
(is (= 500 (:status response)))))
|
||||
|
||||
(testing "content negotiation"
|
||||
(let [response ((app) (-> (request :post "/api/math/plus")
|
||||
(body (pr-str {:x 10, :y 6}))
|
||||
(content-type "application/edn")
|
||||
(header "accept" "application/transit+json")))]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= {:total 16} (m/decode-response-body response)))))))
|
9
test/cljs/pastoralist/core_test.cljs
Normal file
9
test/cljs/pastoralist/core_test.cljs
Normal file
|
@ -0,0 +1,9 @@
|
|||
(ns pastoralist.core-test
|
||||
(:require [cljs.test :refer-macros [is are deftest testing use-fixtures]]
|
||||
[pjstadig.humane-test-output]
|
||||
[reagent.core :as reagent :refer [atom]]
|
||||
[pastoralist.core :as rc]))
|
||||
|
||||
(deftest test-home
|
||||
(is (= true true)))
|
||||
|
6
test/cljs/pastoralist/doo_runner.cljs
Normal file
6
test/cljs/pastoralist/doo_runner.cljs
Normal file
|
@ -0,0 +1,6 @@
|
|||
(ns pastoralist.doo-runner
|
||||
(:require [doo.runner :refer-macros [doo-tests]]
|
||||
[pastoralist.core-test]))
|
||||
|
||||
(doo-tests 'pastoralist.core-test)
|
||||
|
Loading…
Reference in a new issue