From c2d1dd3227a8cba914cb55b6ad78a68cf97b3bb1 Mon Sep 17 00:00:00 2001 From: Aaron Fischer Date: Thu, 16 Feb 2017 22:31:16 +0100 Subject: [PATCH] Time to start over +cljs +sqlite +sassc +reagent --- .gitignore | 12 ++ Capstanfile | 28 ++++ Dockerfile | 8 + Procfile | 1 + README.md | 21 +++ env/dev/clj/user.clj | 18 +++ env/dev/clj/yenu/dev_middleware.clj | 10 ++ env/dev/clj/yenu/env.clj | 14 ++ env/dev/clj/yenu/figwheel.clj | 12 ++ env/dev/cljs/yenu/app.cljs | 14 ++ env/dev/resources/config.edn | 4 + env/dev/resources/logback.xml | 43 ++++++ env/prod/clj/yenu/env.clj | 11 ++ env/prod/cljs/yenu/app.cljs | 7 + env/prod/resources/config.edn | 2 + env/prod/resources/logback.xml | 31 ++++ env/test/resources/config.edn | 4 + env/test/resources/logback.xml | 43 ++++++ project.clj | 139 ++++++++++++++++++ resources/docs/docs.md | 137 +++++++++++++++++ .../20170216222925-add-users-table.down.sql | 1 + .../20170216222925-add-users-table.up.sql | 9 ++ resources/public/css/screen.css | 69 +++++++++ resources/public/favicon.ico | Bin 0 -> 1150 bytes resources/public/img/warning_clojure.png | Bin 0 -> 21846 bytes resources/scss/screen.scss | 5 + resources/sql/queries.sql | 21 +++ resources/templates/error.html | 56 +++++++ resources/templates/home.html | 46 ++++++ src/clj/yenu/config.clj | 10 ++ src/clj/yenu/core.clj | 58 ++++++++ src/clj/yenu/db/core.clj | 12 ++ src/clj/yenu/handler.clj | 25 ++++ src/clj/yenu/layout.clj | 38 +++++ src/clj/yenu/middleware.clj | 66 +++++++++ src/clj/yenu/routes/home.clj | 16 ++ src/cljc/yenu/validation.cljc | 3 + src/cljs/yenu/ajax.cljs | 20 +++ src/cljs/yenu/core.cljs | 86 +++++++++++ test/clj/yenu/test/db/core.clj | 36 +++++ test/clj/yenu/test/handler.clj | 13 ++ test/cljs/yenu/core_test.cljs | 9 ++ test/cljs/yenu/doo_runner.cljs | 6 + 43 files changed, 1164 insertions(+) create mode 100644 .gitignore create mode 100644 Capstanfile create mode 100644 Dockerfile create mode 100644 Procfile create mode 100644 README.md create mode 100644 env/dev/clj/user.clj create mode 100644 env/dev/clj/yenu/dev_middleware.clj create mode 100644 env/dev/clj/yenu/env.clj create mode 100644 env/dev/clj/yenu/figwheel.clj create mode 100644 env/dev/cljs/yenu/app.cljs create mode 100644 env/dev/resources/config.edn create mode 100644 env/dev/resources/logback.xml create mode 100644 env/prod/clj/yenu/env.clj create mode 100644 env/prod/cljs/yenu/app.cljs create mode 100644 env/prod/resources/config.edn create mode 100644 env/prod/resources/logback.xml create mode 100644 env/test/resources/config.edn create mode 100644 env/test/resources/logback.xml create mode 100644 project.clj create mode 100644 resources/docs/docs.md create mode 100644 resources/migrations/20170216222925-add-users-table.down.sql create mode 100644 resources/migrations/20170216222925-add-users-table.up.sql create mode 100644 resources/public/css/screen.css create mode 100644 resources/public/favicon.ico create mode 100644 resources/public/img/warning_clojure.png create mode 100644 resources/scss/screen.scss create mode 100644 resources/sql/queries.sql create mode 100644 resources/templates/error.html create mode 100644 resources/templates/home.html create mode 100644 src/clj/yenu/config.clj create mode 100644 src/clj/yenu/core.clj create mode 100644 src/clj/yenu/db/core.clj create mode 100644 src/clj/yenu/handler.clj create mode 100644 src/clj/yenu/layout.clj create mode 100644 src/clj/yenu/middleware.clj create mode 100644 src/clj/yenu/routes/home.clj create mode 100644 src/cljc/yenu/validation.cljc create mode 100644 src/cljs/yenu/ajax.cljs create mode 100644 src/cljs/yenu/core.cljs create mode 100644 test/clj/yenu/test/db/core.clj create mode 100644 test/clj/yenu/test/handler.clj create mode 100644 test/cljs/yenu/core_test.cljs create mode 100644 test/cljs/yenu/doo_runner.cljs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94d24c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/target +/lib +/classes +/checkouts +pom.xml +*.jar +*.class +/.lein-* +profiles.clj +/.env +.nrepl-port +/log diff --git a/Capstanfile b/Capstanfile new file mode 100644 index 0000000..29c4d94 --- /dev/null +++ b/Capstanfile @@ -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 /yenu/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: + /yenu/app.jar: ./target/uberjar/yenu.jar + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75fe433 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM java:8-alpine +MAINTAINER Your Name + +ADD target/uberjar/yenu.jar /yenu/app.jar + +EXPOSE 3000 + +CMD ["java", "-jar", "/yenu/app.jar"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1c897a9 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java $JVM_OPTS -cp target/uberjar/yenu.jar clojure.main -m yenu.core diff --git a/README.md b/README.md new file mode 100644 index 0000000..04f1d90 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# yenu + +generated using Luminus version "2.9.11.32" + +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 © 2017 FIXME diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj new file mode 100644 index 0000000..549a7fd --- /dev/null +++ b/env/dev/clj/user.clj @@ -0,0 +1,18 @@ +(ns user + (:require [mount.core :as mount] + [yenu.figwheel :refer [start-fw stop-fw cljs]] + yenu.core)) + +(defn start [] + (mount/start-without #'yenu.core/http-server + #'yenu.core/repl-server)) + +(defn stop [] + (mount/stop-except #'yenu.core/http-server + #'yenu.core/repl-server)) + +(defn restart [] + (stop) + (start)) + + diff --git a/env/dev/clj/yenu/dev_middleware.clj b/env/dev/clj/yenu/dev_middleware.clj new file mode 100644 index 0000000..53684cb --- /dev/null +++ b/env/dev/clj/yenu/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns yenu.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)) diff --git a/env/dev/clj/yenu/env.clj b/env/dev/clj/yenu/env.clj new file mode 100644 index 0000000..82154df --- /dev/null +++ b/env/dev/clj/yenu/env.clj @@ -0,0 +1,14 @@ +(ns yenu.env + (:require [selmer.parser :as parser] + [clojure.tools.logging :as log] + [yenu.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (log/info "\n-=[yenu started successfully using the development profile]=-")) + :stop + (fn [] + (log/info "\n-=[yenu has shut down successfully]=-")) + :middleware wrap-dev}) diff --git a/env/dev/clj/yenu/figwheel.clj b/env/dev/clj/yenu/figwheel.clj new file mode 100644 index 0000000..d25cd94 --- /dev/null +++ b/env/dev/clj/yenu/figwheel.clj @@ -0,0 +1,12 @@ +(ns yenu.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)) + diff --git a/env/dev/cljs/yenu/app.cljs b/env/dev/cljs/yenu/app.cljs new file mode 100644 index 0000000..5a78f30 --- /dev/null +++ b/env/dev/cljs/yenu/app.cljs @@ -0,0 +1,14 @@ +(ns ^:figwheel-no-load yenu.app + (:require [yenu.core :as core] + [devtools.core :as devtools] + [figwheel.client :as figwheel :include-macros true])) + +(enable-console-print!) + +(figwheel/watch-and-reload + :websocket-url "ws://localhost:3449/figwheel-ws" + :on-jsload core/mount-components) + +(devtools/install!) + +(core/init!) diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn new file mode 100644 index 0000000..6f14c22 --- /dev/null +++ b/env/dev/resources/config.edn @@ -0,0 +1,4 @@ +{:dev true + :port 3000 + ;; when :nrepl-port is set the application starts the nREPL server on load + :nrepl-port 7000} diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 0000000..807263d --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,43 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/yenu.log + + log/yenu.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + + + + + + + + diff --git a/env/prod/clj/yenu/env.clj b/env/prod/clj/yenu/env.clj new file mode 100644 index 0000000..c9879e4 --- /dev/null +++ b/env/prod/clj/yenu/env.clj @@ -0,0 +1,11 @@ +(ns yenu.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init + (fn [] + (log/info "\n-=[yenu started successfully]=-")) + :stop + (fn [] + (log/info "\n-=[yenu has shut down successfully]=-")) + :middleware identity}) diff --git a/env/prod/cljs/yenu/app.cljs b/env/prod/cljs/yenu/app.cljs new file mode 100644 index 0000000..d253205 --- /dev/null +++ b/env/prod/cljs/yenu/app.cljs @@ -0,0 +1,7 @@ +(ns yenu.app + (:require [yenu.core :as core])) + +;;ignore println statements in prod +(set! *print-fn* (fn [& _])) + +(core/init!) diff --git a/env/prod/resources/config.edn b/env/prod/resources/config.edn new file mode 100644 index 0000000..b48cfbd --- /dev/null +++ b/env/prod/resources/config.edn @@ -0,0 +1,2 @@ +{:production true + :port 3000} diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml new file mode 100644 index 0000000..95f05ec --- /dev/null +++ b/env/prod/resources/logback.xml @@ -0,0 +1,31 @@ + + + + + log/yenu.log + + log/yenu.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + + + + diff --git a/env/test/resources/config.edn b/env/test/resources/config.edn new file mode 100644 index 0000000..6f14c22 --- /dev/null +++ b/env/test/resources/config.edn @@ -0,0 +1,4 @@ +{:dev true + :port 3000 + ;; when :nrepl-port is set the application starts the nREPL server on load + :nrepl-port 7000} diff --git a/env/test/resources/logback.xml b/env/test/resources/logback.xml new file mode 100644 index 0000000..807263d --- /dev/null +++ b/env/test/resources/logback.xml @@ -0,0 +1,43 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/yenu.log + + log/yenu.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + + + + + + + + diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..66e2241 --- /dev/null +++ b/project.clj @@ -0,0 +1,139 @@ +(defproject yenu "0.1.0-SNAPSHOT" + + :description "FIXME: write description" + :url "http://example.com/FIXME" + + :dependencies [[bouncer "1.0.0"] + [cljs-ajax "0.5.8"] + [compojure "1.5.2"] + [conman "0.6.3"] + [cprop "0.1.10"] + [luminus-immutant "0.2.3"] + [luminus-migrations "0.2.9"] + [luminus-nrepl "0.1.4"] + [markdown-clj "0.9.94"] + [metosin/ring-http-response "0.8.1"] + [mount "0.1.11"] + [org.clojure/clojure "1.8.0"] + [org.clojure/clojurescript "1.9.473" :scope "provided"] + [org.clojure/tools.cli "0.3.5"] + [org.clojure/tools.logging "0.3.1"] + [org.webjars.bower/tether "1.4.0"] + [org.webjars/bootstrap "4.0.0-alpha.5"] + [org.webjars/font-awesome "4.7.0"] + [org.webjars/webjars-locator-jboss-vfs "0.1.0"] + [org.xerial/sqlite-jdbc "3.16.1"] + [reagent "0.6.0"] + [reagent-utils "0.2.0"] + [ring-middleware-format "0.7.2"] + [ring-webjars "0.1.1"] + [ring/ring-core "1.5.1"] + [ring/ring-defaults "0.2.3"] + [secretary "1.2.3"] + [selmer "1.10.6"]] + + :min-lein-version "2.0.0" + + :jvm-opts ["-server" "-Dconf=.lein-env"] + :source-paths ["src/clj" "src/cljc"] + :resource-paths ["resources" "target/cljsbuild"] + :target-path "target/%s/" + :main yenu.core + :migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")} + + :plugins [[lein-cprop "1.0.1"] + [migratus-lein "0.4.3"] + [lein-cljsbuild "1.1.4"] + [lein-immutant "2.1.0"] + [lein-sassc "0.10.4"] + [lein-auto "0.1.2"]] + :sassc + [{:src "resources/scss/screen.scss" + :output-to "resources/public/css/screen.css" + :style "nested" + :import-path "resources/scss"}] + + :auto + {"sassc" {:file-pattern #"\.(scss|sass)$" :paths ["resources/scss"]}} + + :hooks [leiningen.sassc] + :clean-targets ^{:protect false} + [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] + :figwheel + {:http-server-root "public" + :nrepl-port 7002 + :css-dirs ["resources/public/css"] + :nrepl-middleware [cemerick.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-to "target/cljsbuild/public/js/app.js" + :optimizations :advanced + :pretty-print false + :closure-warnings + {:externs-validation :off :non-standard-jsdoc :off} + :externs ["react/externs/react.js"]}}}} + + + :aot :all + :uberjar-name "yenu.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 {:dependencies [[prone "1.1.4"] + [ring/ring-mock "0.3.0"] + [ring/ring-devel "1.5.1"] + [pjstadig/humane-test-output "0.8.1"] + [binaryage/devtools "0.9.0"] + [com.cemerick/piggieback "0.2.2-SNAPSHOT"] + [doo "0.1.7"] + [figwheel-sidecar "0.5.9"]] + :plugins [[com.jakemccrary/lein-test-refresh "0.18.1"] + [lein-doo "0.1.7"] + [lein-figwheel "0.5.9"] + [org.clojure/clojurescript "1.9.473"]] + :cljsbuild + {:builds + {:app + {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] + :compiler + {:main "yenu.app" + :asset-path "/js/out" + :output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js/out" + :source-map true + :optimizations :none + :pretty-print true}}}} + + + + :doo {:build "test"} + :source-paths ["env/dev/clj" "test/clj"] + :resource-paths ["env/dev/resources"] + :repl-options {:init-ns user} + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)]} + :project/test {:resource-paths ["env/test/resources"] + :cljsbuild + {:builds + {:test + {:source-paths ["src/cljc" "src/cljs" "test/cljs"] + :compiler + {:output-to "target/test.js" + :main "yenu.doo-runner" + :optimizations :whitespace + :pretty-print true}}}} + + } + :profiles/dev {} + :profiles/test {}}) diff --git a/resources/docs/docs.md b/resources/docs/docs.md new file mode 100644 index 0000000..b4b4a6d --- /dev/null +++ b/resources/docs/docs.md @@ -0,0 +1,137 @@ +

Congratulations, your Luminus site is ready!

+ +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 `yenu.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. + +``` +(defroutes home-routes + (GET "/" [] + (home-page)) + (GET "/docs" [] + (-> (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 `yenu.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/yenu/core.cljs` file. + + + +#### Organizing the routes + +The routes are aggregated and wrapped with middleware in the `yenu.handler` namespace: + +``` +(def app-routes + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"}))))) +``` + +The `app-routes` definition groups all the routes in the application into a single handler. +A default route group is added to handle the `404` case. + +learn more about routing » + +The `home-routes` are wrapped with two middleware functions. The first enables CSRF protection. +The second takes care of serializing and deserializing various encoding formats, such as JSON. + +#### Managing your middleware + +Request middleware functions are located under the `yenu.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 `yenu.dev-middleware` namespace found in +the `env/dev/clj/` source path. + +learn more about middleware » + +
+ +#### 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 `profiles.clj` file with your database name and login. +* 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 `yenu.db.core` in some other namespace. +* Restart the application. + +learn more about database access » + +
+ +
+ +#### SassC libsass command-line compiler is required + +You must have the SassC command-line compiler installed to use this feature. + +Please follow the instructions at: http://github.com/sass/sassc +to install the compiler for your platform. + +#### Usage +Compile your files once: +``` +$ lein sassc once +``` + +To delete all the files generated by lein-sassc: +``` +$ lein sassc clean +``` + +To recompile when any changes are made: + +``` +$ lein auto sassc once +``` + +#### Hooks +The following hooks are supported by lein-sassc: +``` +$ lein compile +$ lein clean +``` + +Because lein-sassc requires a binary to compile Sass, it often won't work on platforms like Heroku which compile the application on their servers. To get around this limitation, commit the generated CSS files and remove + +``` +:hooks [leiningen.sassc] +``` + +from project.clj. + +
+ + +#### 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. diff --git a/resources/migrations/20170216222925-add-users-table.down.sql b/resources/migrations/20170216222925-add-users-table.down.sql new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/resources/migrations/20170216222925-add-users-table.down.sql @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/resources/migrations/20170216222925-add-users-table.up.sql b/resources/migrations/20170216222925-add-users-table.up.sql new file mode 100644 index 0000000..05ad4af --- /dev/null +++ b/resources/migrations/20170216222925-add-users-table.up.sql @@ -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 TIME, + is_active BOOLEAN, + pass VARCHAR(300)); diff --git a/resources/public/css/screen.css b/resources/public/css/screen.css new file mode 100644 index 0000000..3a8307f --- /dev/null +++ b/resources/public/css/screen.css @@ -0,0 +1,69 @@ +html, +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; +} +.navbar { + margin-bottom: 10px; + border-radius: 0px; +} +.navbar-brand { + float: none; +} +.navbar-nav .nav-item { + float: none; +} +.navbar-divider, +.navbar-nav .nav-item+.nav-item, +.navbar-nav .nav-link + .nav-link { + margin-left: 0; +} +@media (min-width: 34em) { + .navbar-brand { + float: left; + } + .navbar-nav .nav-item { + float: left; + } + .navbar-divider, + .navbar-nav .nav-item+.nav-item, + .navbar-nav .nav-link + .nav-link { + margin-left: 1rem; + } +} + +@-moz-keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes three-quarters-loader { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + diff --git a/resources/public/favicon.ico b/resources/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0e50cb2fb96b2ae63a2cf81adf4e1b3869cfe152 GIT binary patch literal 1150 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 P-(WnP}f5EZ_VG%x##xBtZkUi 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 literal 0 HcmV?d00001 diff --git a/resources/scss/screen.scss b/resources/scss/screen.scss new file mode 100644 index 0000000..7f38ace --- /dev/null +++ b/resources/scss/screen.scss @@ -0,0 +1,5 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; + padding-top: 40px; +} diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql new file mode 100644 index 0000000..4191f67 --- /dev/null +++ b/resources/sql/queries.sql @@ -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 update 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 retrieve a user given the id. +SELECT * FROM users +WHERE id = :id + +-- :name delete-user! :! :n +-- :doc delete a user given the id +DELETE FROM users +WHERE id = :id diff --git a/resources/templates/error.html b/resources/templates/error.html new file mode 100644 index 0000000..6fcd237 --- /dev/null +++ b/resources/templates/error.html @@ -0,0 +1,56 @@ + + + + Something bad happened + + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/bootstrap/css/bootstrap-theme.min.css" %} + + + +
+
+
+
+
+

Error: {{status}}

+
+ {% if title %} +

{{title}}

+ {% endif %} + {% if message %} +

{{message}}

+ {% endif %} +
+
+
+
+
+ + diff --git a/resources/templates/home.html b/resources/templates/home.html new file mode 100644 index 0000000..33d8d3e --- /dev/null +++ b/resources/templates/home.html @@ -0,0 +1,46 @@ + + + + + + Welcome to yenu + + + +
+ + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/font-awesome/css/font-awesome.min.css" %} + {% style "/css/screen.css" %} + + + {% script "/js/app.js" %} + + + + + + diff --git a/src/clj/yenu/config.clj b/src/clj/yenu/config.clj new file mode 100644 index 0000000..a44fe8a --- /dev/null +++ b/src/clj/yenu/config.clj @@ -0,0 +1,10 @@ +(ns yenu.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)])) diff --git a/src/clj/yenu/core.clj b/src/clj/yenu/core.clj new file mode 100644 index 0000000..656c739 --- /dev/null +++ b/src/clj/yenu/core.clj @@ -0,0 +1,58 @@ +(ns yenu.core + (:require [yenu.handler :as handler] + [luminus.repl-server :as repl] + [luminus.http-server :as http] + [luminus-migrations.core :as migrations] + [yenu.config :refer [env]] + [clojure.tools.cli :refer [parse-opts]] + [clojure.tools.logging :as log] + [mount.core :as mount]) + (:gen-class)) + +(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 :port #(or (-> env :options :port) %)))) + :stop + (http/stop http-server)) + +(mount/defstate ^{:on-reload :noop} + repl-server + :start + (when-let [nrepl-port (env :nrepl-port)] + (repl/start {:port nrepl-port})) + :stop + (when repl-server + (repl/stop repl-server))) + + +(defn stop-app [] + (doseq [component (:stopped (mount/stop))] + (log/info component "stopped")) + (shutdown-agents)) + +(defn start-app [args] + (doseq [component (-> args + (parse-opts cli-options) + mount/start-with-args + :started)] + (log/info component "started")) + (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app))) + +(defn -main [& args] + (cond + (some #{"migrate" "rollback"} args) + (do + (mount/start #'yenu.config/env) + (migrations/migrate args (select-keys env [:database-url])) + (System/exit 0)) + :else + (start-app args))) + diff --git a/src/clj/yenu/db/core.clj b/src/clj/yenu/db/core.clj new file mode 100644 index 0000000..f305535 --- /dev/null +++ b/src/clj/yenu/db/core.clj @@ -0,0 +1,12 @@ +(ns yenu.db.core + (:require + [conman.core :as conman] + [mount.core :refer [defstate]] + [yenu.config :refer [env]])) + +(defstate ^:dynamic *db* + :start (conman/connect! {:jdbc-url (env :database-url)}) + :stop (conman/disconnect! *db*)) + +(conman/bind-connection *db* "sql/queries.sql") + diff --git a/src/clj/yenu/handler.clj b/src/clj/yenu/handler.clj new file mode 100644 index 0000000..613677f --- /dev/null +++ b/src/clj/yenu/handler.clj @@ -0,0 +1,25 @@ +(ns yenu.handler + (:require [compojure.core :refer [routes wrap-routes]] + [yenu.layout :refer [error-page]] + [yenu.routes.home :refer [home-routes]] + [compojure.route :as route] + [yenu.env :refer [defaults]] + [mount.core :as mount] + [yenu.middleware :as middleware])) + +(mount/defstate init-app + :start ((or (:init defaults) identity)) + :stop ((or (:stop defaults) identity))) + +(def app-routes + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"}))))) + + +(defn app [] (middleware/wrap-base #'app-routes)) diff --git a/src/clj/yenu/layout.clj b/src/clj/yenu/layout.clj new file mode 100644 index 0000000..4dc0d7e --- /dev/null +++ b/src/clj/yenu/layout.clj @@ -0,0 +1,38 @@ +(ns yenu.layout + (:require [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*]])) + +(declare ^:dynamic *app-context*) +(parser/set-resource-path! (clojure.java.io/resource "templates")) +(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/templates" + [template & [params]] + (content-type + (ok + (parser/render-file + template + (assoc params + :page template + :csrf-token *anti-forgery-token* + :servlet-context *app-context*))) + "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)}) diff --git a/src/clj/yenu/middleware.clj b/src/clj/yenu/middleware.clj new file mode 100644 index 0000000..bce0d00 --- /dev/null +++ b/src/clj/yenu/middleware.clj @@ -0,0 +1,66 @@ +(ns yenu.middleware + (:require [yenu.env :refer [defaults]] + [clojure.tools.logging :as log] + [yenu.layout :refer [*app-context* error-page]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.webjars :refer [wrap-webjars]] + [ring.middleware.format :refer [wrap-restful-format]] + [yenu.config :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]]) + (:import [javax.servlet ServletContext])) + +(defn wrap-context [handler] + (fn [request] + (binding [*app-context* + (if-let [context (:servlet-context request)] + ;; If we're not inside a servlet environment + ;; (for example when using mock requests), then + ;; .getContextPath might not exist + (try (.getContextPath ^ServletContext context) + (catch IllegalArgumentException _ context)) + ;; if the context is not specified in the request + ;; we check if one has been specified in the environment + ;; instead + (:app-context env))] + (handler request)))) + +(defn wrap-internal-error [handler] + (fn [req] + (try + (handler req) + (catch Throwable t + (log/error 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 (wrap-restful-format + handler + {:formats [:json-kw :transit-json :transit-msgpack]})] + (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-webjars + wrap-flash + (wrap-session {:cookie-attrs {:http-only true}}) + (wrap-defaults + (-> site-defaults + (assoc-in [:security :anti-forgery] false) + (dissoc :session))) + wrap-context + wrap-internal-error)) diff --git a/src/clj/yenu/routes/home.clj b/src/clj/yenu/routes/home.clj new file mode 100644 index 0000000..e44c21a --- /dev/null +++ b/src/clj/yenu/routes/home.clj @@ -0,0 +1,16 @@ +(ns yenu.routes.home + (:require [yenu.layout :as layout] + [compojure.core :refer [defroutes GET]] + [ring.util.http-response :as response] + [clojure.java.io :as io])) + +(defn home-page [] + (layout/render "home.html")) + +(defroutes home-routes + (GET "/" [] + (home-page)) + (GET "/docs" [] + (-> (response/ok (-> "docs/docs.md" io/resource slurp)) + (response/header "Content-Type" "text/plain; charset=utf-8")))) + diff --git a/src/cljc/yenu/validation.cljc b/src/cljc/yenu/validation.cljc new file mode 100644 index 0000000..71e31b2 --- /dev/null +++ b/src/cljc/yenu/validation.cljc @@ -0,0 +1,3 @@ +(ns yenu.validation + (:require [bouncer.core :as b] + [bouncer.validators :as v])) diff --git a/src/cljs/yenu/ajax.cljs b/src/cljs/yenu/ajax.cljs new file mode 100644 index 0000000..06d8c55 --- /dev/null +++ b/src/cljs/yenu/ajax.cljs @@ -0,0 +1,20 @@ +(ns yenu.ajax + (:require [ajax.core :as ajax])) + +(defn local-uri? [{:keys [uri]}] + (not (re-find #"^\w+?://" uri))) + +(defn default-headers [request] + (if (local-uri? request) + (-> request + (update :uri #(str js/context %)) + (update :headers #(merge {"x-csrf-token" js/csrfToken} %))) + request)) + +(defn load-interceptors! [] + (swap! ajax/default-interceptors + conj + (ajax/to-interceptor {:name "default headers" + :request default-headers}))) + + diff --git a/src/cljs/yenu/core.cljs b/src/cljs/yenu/core.cljs new file mode 100644 index 0000000..295194c --- /dev/null +++ b/src/cljs/yenu/core.cljs @@ -0,0 +1,86 @@ +(ns yenu.core + (:require [reagent.core :as r] + [reagent.session :as session] + [secretary.core :as secretary :include-macros true] + [goog.events :as events] + [goog.history.EventType :as HistoryEventType] + [markdown.core :refer [md->html]] + [yenu.ajax :refer [load-interceptors!]] + [ajax.core :refer [GET POST]]) + (:import goog.History)) + +(defn nav-link [uri title page collapsed?] + [:li.nav-item + {:class (when (= page (session/get :page)) "active")} + [:a.nav-link + {:href uri + :on-click #(reset! collapsed? true)} title]]) + +(defn navbar [] + (let [collapsed? (r/atom true)] + (fn [] + [:nav.navbar.navbar-dark.bg-primary + [:button.navbar-toggler.hidden-sm-up + {:on-click #(swap! collapsed? not)} "☰"] + [:div.collapse.navbar-toggleable-xs + (when-not @collapsed? {:class "in"}) + [:a.navbar-brand {:href "#/"} "yenu"] + [:ul.nav.navbar-nav + [nav-link "#/" "Home" :home collapsed?] + [nav-link "#/about" "About" :about collapsed?]]]]))) + +(defn about-page [] + [:div.container + [:div.row + [:div.col-md-12 + [:img {:src (str js/context "/img/warning_clojure.png")}]]]]) + +(defn home-page [] + [:div.container + (when-let [docs (session/get :docs)] + [:div.row>div.col-sm-12 + [:div {:dangerouslySetInnerHTML + {:__html (md->html docs)}}]])]) + +(def pages + {:home #'home-page + :about #'about-page}) + +(defn page [] + [(pages (session/get :page))]) + +;; ------------------------- +;; Routes +(secretary/set-config! :prefix "#") + +(secretary/defroute "/" [] + (session/put! :page :home)) + +(secretary/defroute "/about" [] + (session/put! :page :about)) + +;; ------------------------- +;; History +;; must be called after routes have been defined +(defn hook-browser-navigation! [] + (doto (History.) + (events/listen + HistoryEventType/NAVIGATE + (fn [event] + (secretary/dispatch! (.-token event)))) + (.setEnabled true))) + +;; ------------------------- +;; Initialize app +(defn fetch-docs! [] + (GET "/docs" {:handler #(session/put! :docs %)})) + +(defn mount-components [] + (r/render [#'navbar] (.getElementById js/document "navbar")) + (r/render [#'page] (.getElementById js/document "app"))) + +(defn init! [] + (load-interceptors!) + (fetch-docs!) + (hook-browser-navigation!) + (mount-components)) diff --git a/test/clj/yenu/test/db/core.clj b/test/clj/yenu/test/db/core.clj new file mode 100644 index 0000000..cf91630 --- /dev/null +++ b/test/clj/yenu/test/db/core.clj @@ -0,0 +1,36 @@ +(ns yenu.test.db.core + (:require [yenu.db.core :refer [*db*] :as db] + [luminus-migrations.core :as migrations] + [clojure.test :refer :all] + [clojure.java.jdbc :as jdbc] + [yenu.config :refer [env]] + [mount.core :as mount])) + +(use-fixtures + :once + (fn [f] + (mount/start + #'yenu.config/env + #'yenu.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"}))))) diff --git a/test/clj/yenu/test/handler.clj b/test/clj/yenu/test/handler.clj new file mode 100644 index 0000000..364cb2d --- /dev/null +++ b/test/clj/yenu/test/handler.clj @@ -0,0 +1,13 @@ +(ns yenu.test.handler + (:require [clojure.test :refer :all] + [ring.mock.request :refer :all] + [yenu.handler :refer :all])) + +(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)))))) diff --git a/test/cljs/yenu/core_test.cljs b/test/cljs/yenu/core_test.cljs new file mode 100644 index 0000000..acd66f5 --- /dev/null +++ b/test/cljs/yenu/core_test.cljs @@ -0,0 +1,9 @@ +(ns yenu.core-test + (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] + [pjstadig.humane-test-output] + [reagent.core :as reagent :refer [atom]] + [yenu.core :as rc])) + +(deftest test-home + (is (= true true))) + diff --git a/test/cljs/yenu/doo_runner.cljs b/test/cljs/yenu/doo_runner.cljs new file mode 100644 index 0000000..fa9d274 --- /dev/null +++ b/test/cljs/yenu/doo_runner.cljs @@ -0,0 +1,6 @@ +(ns yenu.doo-runner + (:require [doo.runner :refer-macros [doo-tests]] + [yenu.core-test])) + +(doo-tests 'yenu.core-test) +