commit 17fe28a1ac9c16fe05140cf3d6b012c770372fa4 Author: flynn Date: Fri Jan 11 23:10:58 2019 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f7d39d --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/target +/lib +/classes +/checkouts +pom.xml +dev-config.edn +test-config.edn +*.jar +*.class +/.lein-* +profiles.clj +/.env +.nrepl-port +/log +.idea \ No newline at end of file diff --git a/.rebel_readline_history b/.rebel_readline_history new file mode 100644 index 0000000..74c15a0 --- /dev/null +++ b/.rebel_readline_history @@ -0,0 +1,8 @@ +1547000855890:(append-svg) +1547001941101:(clean-builds) +1547001987967:(reset-autobuild) +1547002493261:cljs:quit +1547049842827:(reset-autobuild) +1547053573677:(reload-config) +1547053584758:(fig-status) +1547053746469:(reload-config) diff --git a/Capstanfile b/Capstanfile new file mode 100644 index 0000000..12e9159 --- /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 /cat/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: + /cat/app.jar: ./target/uberjar/cat.jar + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..63e88f7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-alpine + +COPY target/uberjar/cat.jar /cat/app.jar + +EXPOSE 3000 + +CMD ["java", "-jar", "/cat/app.jar"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..3da82eb --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java -cp target/uberjar/cat.jar clojure.main -m cat.core diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8d85c2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# cat + +generated using Luminus version "3.10.29" +init with options: postgres, cljs, auth, oauth, site, kibit + + +## Prerequisites + +You will need [Leiningen][1] 2.0 or above installed. + +[1]: https://github.com/technomancy/leiningen + +## Running + +Copy `dev-config.edn_example` to `dev-config.edn` and fill in the needed fields + +`test-config.edn` is used for test execution. + +To start a web server for the application, run: + + lein run + +To start the ui live rendering, run: + + lein figwheel + +## License + +Copyright © 2019 FIXME \ No newline at end of file diff --git a/cat.iml b/cat.iml new file mode 100644 index 0000000..c1a11cd --- /dev/null +++ b/cat.iml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dev-config.edn_example b/dev-config.edn_example new file mode 100644 index 0000000..0cd4687 --- /dev/null +++ b/dev-config.edn_example @@ -0,0 +1,15 @@ +;; 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:5432/a_db_name?user=a_username&password=a_password"} \ No newline at end of file diff --git a/env/dev/clj/cat/dev_middleware.clj b/env/dev/clj/cat/dev_middleware.clj new file mode 100644 index 0000000..8fc938a --- /dev/null +++ b/env/dev/clj/cat/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns cat.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 ['cat]}))) diff --git a/env/dev/clj/cat/env.clj b/env/dev/clj/cat/env.clj new file mode 100644 index 0000000..e690897 --- /dev/null +++ b/env/dev/clj/cat/env.clj @@ -0,0 +1,14 @@ +(ns cat.env + (:require [selmer.parser :as parser] + [clojure.tools.logging :as log] + [cat.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (log/info "\n-=[cat started successfully using the development profile]=-")) + :stop + (fn [] + (log/info "\n-=[cat has shut down successfully]=-")) + :middleware wrap-dev}) diff --git a/env/dev/clj/cat/figwheel.clj b/env/dev/clj/cat/figwheel.clj new file mode 100644 index 0000000..e23b001 --- /dev/null +++ b/env/dev/clj/cat/figwheel.clj @@ -0,0 +1,12 @@ +(ns cat.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/clj/user.clj b/env/dev/clj/user.clj new file mode 100644 index 0000000..713596a --- /dev/null +++ b/env/dev/clj/user.clj @@ -0,0 +1,42 @@ +(ns user + (:require [cat.config :refer [env]] + [clojure.spec.alpha :as s] + [expound.alpha :as expound] + [mount.core :as mount] + [cat.figwheel :refer [start-fw stop-fw cljs]] + [cat.core :refer [start-app]] + [cat.db.core] + [conman.core :as conman] + [luminus-migrations.core :as migrations])) + +(alter-var-root #'s/*explain-out* (constantly expound/printer)) + +(defn start [] + (mount/start-without #'cat.core/repl-server)) + +(defn stop [] + (mount/stop-except #'cat.core/repl-server)) + +(defn restart [] + (stop) + (start)) + +(defn restart-db [] + (mount/stop #'cat.db.core/*db*) + (mount/start #'cat.db.core/*db*) + (binding [*ns* 'cat.db.core] + (conman/bind-connection cat.db.core/*db* "sql/queries.sql"))) + +(defn reset-db [] + (migrations/migrate ["reset"] (select-keys env [:database-url]))) + +(defn migrate [] + (migrations/migrate ["migrate"] (select-keys env [:database-url]))) + +(defn rollback [] + (migrations/migrate ["rollback"] (select-keys env [:database-url]))) + +(defn create-migration [name] + (migrations/create name (select-keys env [:database-url]))) + + diff --git a/env/dev/cljs/cat/app.cljs b/env/dev/cljs/cat/app.cljs new file mode 100644 index 0000000..20ad623 --- /dev/null +++ b/env/dev/cljs/cat/app.cljs @@ -0,0 +1,13 @@ +(ns ^:figwheel-no-load cat.app + (:require [cat.core :as core] + [cljs.spec.alpha :as s] + [expound.alpha :as expound] + [devtools.core :as devtools])) + +(set! s/*explain-out* expound/printer) + +(enable-console-print!) + +(devtools/install!) + +(core/init!) diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/env/dev/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 0000000..7544e8f --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/cat.log + + log/cat.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + diff --git a/env/prod/clj/cat/env.clj b/env/prod/clj/cat/env.clj new file mode 100644 index 0000000..7a910cf --- /dev/null +++ b/env/prod/clj/cat/env.clj @@ -0,0 +1,11 @@ +(ns cat.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init + (fn [] + (log/info "\n-=[cat started successfully]=-")) + :stop + (fn [] + (log/info "\n-=[cat has shut down successfully]=-")) + :middleware identity}) diff --git a/env/prod/cljs/cat/app.cljs b/env/prod/cljs/cat/app.cljs new file mode 100644 index 0000000..2cb7468 --- /dev/null +++ b/env/prod/cljs/cat/app.cljs @@ -0,0 +1,7 @@ +(ns cat.app + (:require [cat.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..e24ec21 --- /dev/null +++ b/env/prod/resources/config.edn @@ -0,0 +1,2 @@ +{:prod true + :port 3000} diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml new file mode 100644 index 0000000..e080645 --- /dev/null +++ b/env/prod/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + log/cat.log + + log/cat.%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..0967ef4 --- /dev/null +++ b/env/test/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/env/test/resources/logback.xml b/env/test/resources/logback.xml new file mode 100644 index 0000000..7544e8f --- /dev/null +++ b/env/test/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/cat.log + + log/cat.%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..c96be70 --- /dev/null +++ b/project.clj @@ -0,0 +1,142 @@ +(defproject cat "0.1.0-SNAPSHOT" + + :description "A cuddle graph for zeus people" + ;:url "http://example.com/FIXME" + + :dependencies [[buddy "2.0.0"] + [cheshire "5.8.1"] + [clj-oauth "1.5.5"] + [clojure.java-time "0.3.2"] + [com.cognitect/transit-clj "0.8.313"] + [compojure "1.6.1"] + [conman "0.8.3"] + [cprop "0.1.13"] + [funcool/struct "1.3.0"] + [luminus-immutant "0.2.4"] + [luminus-migrations "0.6.3"] + [luminus-transit "0.1.1"] + [luminus/ring-ttl-session "0.3.2"] + [markdown-clj "1.0.5"] + [metosin/muuntaja "0.6.3"] + [metosin/ring-http-response "0.9.1"] + [mount "0.1.15"] + [nrepl "0.5.3"] + [org.clojure/clojure "1.10.0"] + [org.clojure/clojurescript "1.10.439" :scope "provided"] + [org.clojure/tools.cli "0.4.1"] + [org.clojure/tools.logging "0.4.1"] + [org.postgresql/postgresql "42.2.5"] + [org.webjars.bower/tether "1.4.4"] + [org.webjars/bootstrap "4.2.1"] + [org.webjars/font-awesome "5.6.1"] + [org.webjars/webjars-locator "0.34"] + [ring-webjars "0.2.0"] + [ring/ring-core "1.7.1"] + [ring/ring-defaults "0.3.2"] + [selmer "1.12.5"] + [metosin/vega-tools "0.2.0"] + [funcool/promesa "1.9.0"]] + + + + :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 cat.core + + :plugins [[lein-cljsbuild "1.1.7"] + [lein-immutant "2.1.0"] + [lein-kibit "0.1.2"]] + :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/wrap-cljs-repl 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}}}}} + + + :aot :all + :uberjar-name "cat.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.3.10"] + [doo "0.1.11"] + [expound "0.7.2"] + [figwheel-sidecar "0.5.18"] + [pjstadig/humane-test-output "0.9.0"] + [prone "1.6.1"] + [ring/ring-devel "1.7.1"] + [ring/ring-mock "0.3.2"]] + :plugins [[com.jakemccrary/lein-test-refresh "0.23.0"] + [lein-doo "0.1.11"] + [lein-figwheel "0.5.18"]] + :cljsbuild + {:builds + {:app + {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] + :figwheel {:on-jsload "cat.core/mount-components"} + :compiler + {:main "cat.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"] + :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 "cat.doo-runner" + :optimizations :whitespace + :pretty-print true}}}}} + + + :profiles/dev {} + :profiles/test {}} + :repl-options { + ;; If nREPL takes too long to load it may timeout, + ;; increase this to wait longer before timing out. + ;; Defaults to 30000 (30 seconds) + :timeout 120000}) + diff --git a/resources/docs/docs.md b/resources/docs/docs.md new file mode 100644 index 0000000..cef016f --- /dev/null +++ b/resources/docs/docs.md @@ -0,0 +1,97 @@ +

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 `cat.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 `cat.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/cat/core.cljs` file. + + + +#### Organizing the routes + +The routes are aggregated and wrapped with middleware in the `cat.handler` namespace: + +``` +(defstate app + :start + (middleware/wrap-base + (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` 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 `cat.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 `cat.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 `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 `cat.db.core` in some other namespace. +* Restart the application. + +learn more about database access » + +
+ + + +#### 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/html/error.html b/resources/html/error.html new file mode 100644 index 0000000..31dd41d --- /dev/null +++ b/resources/html/error.html @@ -0,0 +1,55 @@ + + + + Something bad happened + + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + + + +
+
+
+
+
+

Error: {{status}}

+
+ {% if title %} +

{{title}}

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

{{message}}

+ {% endif %} +
+
+
+
+
+ + diff --git a/resources/html/home.html b/resources/html/home.html new file mode 100644 index 0000000..393c5d3 --- /dev/null +++ b/resources/html/home.html @@ -0,0 +1,123 @@ + + + + + + Welcome to cat + + + + +
+
+
+
+
+

Add person

+
+ {% csrf-field %} +
+
+
+ + +
+
+
+

Add relation (please enter correct id's of the person)

+
+ {% csrf-field %} +
+
+
+ +
+
+
+ +
+
+ +
+
+
+

Users

+
+
Name
+
Gender
+ {% for user in users %} +
+ {{user.name}} +
+
+ {{user.gender}} +
+ {% endfor %} +
+
+
+

Relations

+
+
Person 1
+
Person 2
+ {% for relation in relations %} +
+ {{relation.name}} +
+
+ {{relation.name_2}} +
+ {% endfor %} +
+
+
+
+
+
+
+
+

Welcome to cat

+

If you're seeing this message, that means you haven't yet compiled your ClojureScript!

+

Please run lein figwheel to start the ClojureScript compiler and reload the page.

+

For better ClojureScript development experience in Chrome follow these steps:

+
    +
  • Open DevTools +
  • Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > + General > Console) +
  • Check-in "Enable custom formatters" +
  • Close DevTools +
  • Open DevTools +
+

See ClojureScript documentation for + further details.

+
+
+
+
+ + +{% style "/assets/bootstrap/css/bootstrap.min.css" %} +{% style "/assets/font-awesome/css/all.css" %} +{% style "/css/screen.css" %} + +{% script "/assets/jquery/jquery.min.js" %} +{% script "/assets/font-awesome/js/all.js" %} +{% script "/assets/tether/dist/js/tether.min.js" %} +{% script "/assets/bootstrap/js/bootstrap.min.js" %} + + +{% script "/js/app.js" %} + +{% script "/js/graphing.js" %} + + diff --git a/resources/migrations/20190109002211-add-users-table.down.sql_example b/resources/migrations/20190109002211-add-users-table.down.sql_example new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/resources/migrations/20190109002211-add-users-table.down.sql_example @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/resources/migrations/20190109002211-add-users-table.up.sql_example b/resources/migrations/20190109002211-add-users-table.up.sql_example new file mode 100644 index 0000000..b9c31f1 --- /dev/null +++ b/resources/migrations/20190109002211-add-users-table.up.sql_example @@ -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)); diff --git a/resources/migrations/20190109004458-add-users-table.down.sql b/resources/migrations/20190109004458-add-users-table.down.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/resources/migrations/20190109004458-add-users-table.down.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/resources/migrations/20190109004458-add-users-table.up.sql b/resources/migrations/20190109004458-add-users-table.up.sql new file mode 100644 index 0000000..787b4ee --- /dev/null +++ b/resources/migrations/20190109004458-add-users-table.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE users +(id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + gender VARCHAR(255)); \ No newline at end of file diff --git a/resources/migrations/20190109005337-add-relation-table.down.sql b/resources/migrations/20190109005337-add-relation-table.down.sql new file mode 100644 index 0000000..5a790e7 --- /dev/null +++ b/resources/migrations/20190109005337-add-relation-table.down.sql @@ -0,0 +1 @@ +DROP TABLE relations; diff --git a/resources/migrations/20190109005337-add-relation-table.up.sql b/resources/migrations/20190109005337-add-relation-table.up.sql new file mode 100644 index 0000000..aac53c1 --- /dev/null +++ b/resources/migrations/20190109005337-add-relation-table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE relations +(id SERIAL PRIMARY KEY, + from_id INTEGER NOT NULL, + to_id INTEGER NOT NULL, + FOREIGN KEY (from_id) REFERENCES users (id), + FOREIGN KEY (to_id) REFERENCES users (id)); \ No newline at end of file diff --git a/resources/public/css/screen.css b/resources/public/css/screen.css new file mode 100644 index 0000000..4184e2c --- /dev/null +++ b/resources/public/css/screen.css @@ -0,0 +1,46 @@ +html, +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; +} +nav { + margin-bottom: 20px; +} + +.grid-container { + display: grid; + grid-template-columns: 1fr 1fr; +} + +.grid-item{ + margin-left: 20px; + margin-bottom: 20px; +} + +.item-container{ + /*https://medium.com/@js_tut/new-things-css-grid-brings-to-the-table-e465cb5d2841*/ + display: grid; + /*grid-template-rows: 100px 100px;*/ + grid-template-columns: 100px 100px; + grid-gap: 10px; + justify-items: unset; + padding: 0 0 20px 10px; +} +.item { + justify-content: start; + border: 1px solid #b1b1b1; + align-items: start; + padding: 10px; +} + +select { + margin-bottom: 10px; +} + +.input-container{ + display: grid; + grid-template-columns: 1fr 1fr; +} +.input-item { + +} \ No newline at end of file diff --git a/resources/public/favicon.ico b/resources/public/favicon.ico new file mode 100644 index 0000000..0e50cb2 Binary files /dev/null and b/resources/public/favicon.ico differ diff --git a/resources/public/img/warning_clojure.png b/resources/public/img/warning_clojure.png new file mode 100644 index 0000000..78d59e9 Binary files /dev/null and b/resources/public/img/warning_clojure.png differ diff --git a/resources/public/js/graphing.js b/resources/public/js/graphing.js new file mode 100644 index 0000000..0395620 --- /dev/null +++ b/resources/public/js/graphing.js @@ -0,0 +1,13 @@ +var view; + +vega.loader() + .load('/js/spec.json') + .then(function(data) { render(JSON.parse(data)); }); + +function render(spec) { + view = new vega.View(vega.parse(spec)) + .renderer('canvas') // set renderer (canvas or svg) + .initialize('#view') // initialize view within parent DOM container + .hover() // enable hover encode set processing + .run(); +} \ No newline at end of file diff --git a/resources/public/js/spec.json b/resources/public/js/spec.json new file mode 100644 index 0000000..33864e0 --- /dev/null +++ b/resources/public/js/spec.json @@ -0,0 +1,325 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v4.json", + "width": 400, + "height": 300, + "padding": { + "top": 25, + "left": 0, + "right": 0, + "bottom": 0 + }, + "autosize": "none", + "signals": [ + { + "name": "hover", + "value": null, + "on": [ + { + "events": "symbol:mouseover", + "update": "datum" + }, + { + "events": "symbol:mouseout", + "update": "null" + } + ] + }, + { + "name": "title", + "value": "", + "update": "hover ? hover.name : '' " + }, + { + "name": "cx", + "update": "width / 2" + }, + { + "name": "cy", + "update": "height / 2" + }, + { + "name": "nodeRadius", + "value": 8, + "bind": { + "input": "range", + "min": 1, + "max": 50, + "step": 1 + } + }, + { + "name": "nodeCharge", + "value": -30, + "bind": { + "input": "range", + "min": -100, + "max": 10, + "step": 1 + } + }, + { + "name": "linkDistance", + "value": 70, + "bind": { + "input": "range", + "min": 5, + "max": 100, + "step": 1 + } + }, + { + "name": "static", + "value": true, + "bind": { + "input": "checkbox" + } + }, + { + "description": "State variable for active node fix status.", + "name": "fix", + "value": false, + "on": [ + { + "events": "symbol:mouseout[!event.buttons], window:mouseup", + "update": "false" + }, + { + "events": "symbol:mouseover", + "update": "fix || true" + }, + { + "events": "[symbol:mousedown, window:mouseup] > window:mousemove!", + "update": "xy()", + "force": true + } + ] + }, + { + "description": "Graph node most recently interacted with.", + "name": "node", + "value": null, + "on": [ + { + "events": "symbol:mouseover", + "update": "fix === true ? item() : node" + } + ] + }, + { + "description": "Flag to restart Force simulation upon data changes.", + "name": "restart", + "value": false, + "on": [ + { + "events": { + "signal": "fix" + }, + "update": "fix && fix.length" + } + ] + } + ], + "data": [ + { + "name": "node-data", + "url": "/relations_zeroed", + "format": { + "type": "json", + "property": "nodes" + } + }, + { + "name": "link-data", + "url": "/relations_zeroed", + "format": { + "type": "json", + "property": "links" + } + } + ], + "scales": [ + { + "name": "color", + "type": "ordinal", + "domain": { + "data": "node-data", + "field": "group" + }, + "range": { + "scheme": "category20c" + } + } + ], + "marks": [ + { + "name": "nodes-group", + "type": "group", + "zindex": 1, + "from": { + "data": "node-data" + }, + "marks": [ + { + "name": "nodes", + "type": "symbol", + "encode": { + "enter": { + "fill": { + "scale": "color", + "field": {"parent": "group"} + }, + "stroke": { + "value": "white" + } + }, + "update": { + "size": { + "signal": "2 * nodeRadius * nodeRadius" + }, + "cursor": { + "value": "pointer" + } + } + } + }, + { + "type": "text", + "interactive": false, + "encode": { + "enter": { + "fill": { + "value": "black" + }, + "fontSize": { + "value": 12 + }, + "align": { + "value": "center" + }, + "text": { + "field": {"parent": "name"} + }, + "y": { + "value": -10 + } + } + } + } + ], + "on": [ + { + "trigger": "fix", + "modify": "node", + "values": "fix === true ? {fx: node.x, fy: node.y} : {fx: fix[0], fy: fix[1]}" + }, + { + "trigger": "!fix", + "modify": "node", + "values": "{fx: null, fy: null}" + } + ], + "transform": [ + { + "type": "force", + "iterations": 300, + "restart": { + "signal": "restart" + }, + "static": { + "signal": "static" + }, + "signal": "force", + "forces": [ + { + "force": "center", + "x": { + "signal": "cx" + }, + "y": { + "signal": "cy" + } + }, + { + "force": "collide", + "radius": { + "signal": "nodeRadius" + } + }, + { + "force": "nbody", + "strength": { + "signal": "nodeCharge" + } + }, + { + "force": "link", + "links": "link-data", + "distance": { + "signal": "linkDistance" + } + } + ] + } + ] + }, + { + "type": "path", + "from": { + "data": "link-data" + }, + "interactive": false, + "encode": { + "update": { + "stroke": { + "value": "#ccc" + }, + "strokeWidth": { + "value": 0.5 + } + } + }, + "transform": [ + { + "type": "linkpath", + "require": { + "signal": "force" + }, + "shape": "line", + "sourceX": "datum.source.x", + "sourceY": "datum.source.y", + "targetX": "datum.target.x", + "targetY": "datum.target.y" + } + ] + }, + { + "type": "text", + "interactive": false, + "encode": { + "enter": { + "x": { + "signal": "width", + "offset": -5 + }, + "y": { + "value": 0 + }, + "fill": { + "value": "black" + }, + "fontSize": { + "value": 20 + }, + "align": { + "value": "right" + } + }, + "update": { + "text": { + "signal": "title" + } + } + } + } + ] +} diff --git a/resources/public/js/vega_tuto_data.json b/resources/public/js/vega_tuto_data.json new file mode 100644 index 0000000..b267455 --- /dev/null +++ b/resources/public/js/vega_tuto_data.json @@ -0,0 +1,1661 @@ +{ + "nodes": [ + { + "name": "Myriel", + "group": 1, + "index": 0 + }, + { + "name": "Napoleon", + "group": 1, + "index": 1 + }, + { + "name": "Mlle.Baptistine", + "group": 1, + "index": 2 + }, + { + "name": "Mme.Magloire", + "group": 1, + "index": 3 + }, + { + "name": "CountessdeLo", + "group": 1, + "index": 4 + }, + { + "name": "Geborand", + "group": 1, + "index": 5 + }, + { + "name": "Champtercier", + "group": 1, + "index": 6 + }, + { + "name": "Cravatte", + "group": 1, + "index": 7 + }, + { + "name": "Count", + "group": 1, + "index": 8 + }, + { + "name": "OldMan", + "group": 1, + "index": 9 + }, + { + "name": "Labarre", + "group": 2, + "index": 10 + }, + { + "name": "Valjean", + "group": 2, + "index": 11 + }, + { + "name": "Marguerite", + "group": 3, + "index": 12 + }, + { + "name": "Mme.deR", + "group": 2, + "index": 13 + }, + { + "name": "Isabeau", + "group": 2, + "index": 14 + }, + { + "name": "Gervais", + "group": 2, + "index": 15 + }, + { + "name": "Tholomyes", + "group": 3, + "index": 16 + }, + { + "name": "Listolier", + "group": 3, + "index": 17 + }, + { + "name": "Fameuil", + "group": 3, + "index": 18 + }, + { + "name": "Blacheville", + "group": 3, + "index": 19 + }, + { + "name": "Favourite", + "group": 3, + "index": 20 + }, + { + "name": "Dahlia", + "group": 3, + "index": 21 + }, + { + "name": "Zephine", + "group": 3, + "index": 22 + }, + { + "name": "Fantine", + "group": 3, + "index": 23 + }, + { + "name": "Mme.Thenardier", + "group": 4, + "index": 24 + }, + { + "name": "Thenardier", + "group": 4, + "index": 25 + }, + { + "name": "Cosette", + "group": 5, + "index": 26 + }, + { + "name": "Javert", + "group": 4, + "index": 27 + }, + { + "name": "Fauchelevent", + "group": 0, + "index": 28 + }, + { + "name": "Bamatabois", + "group": 2, + "index": 29 + }, + { + "name": "Perpetue", + "group": 3, + "index": 30 + }, + { + "name": "Simplice", + "group": 2, + "index": 31 + }, + { + "name": "Scaufflaire", + "group": 2, + "index": 32 + }, + { + "name": "Woman1", + "group": 2, + "index": 33 + }, + { + "name": "Judge", + "group": 2, + "index": 34 + }, + { + "name": "Champmathieu", + "group": 2, + "index": 35 + }, + { + "name": "Brevet", + "group": 2, + "index": 36 + }, + { + "name": "Chenildieu", + "group": 2, + "index": 37 + }, + { + "name": "Cochepaille", + "group": 2, + "index": 38 + }, + { + "name": "Pontmercy", + "group": 4, + "index": 39 + }, + { + "name": "Boulatruelle", + "group": 6, + "index": 40 + }, + { + "name": "Eponine", + "group": 4, + "index": 41 + }, + { + "name": "Anzelma", + "group": 4, + "index": 42 + }, + { + "name": "Woman2", + "group": 5, + "index": 43 + }, + { + "name": "MotherInnocent", + "group": 0, + "index": 44 + }, + { + "name": "Gribier", + "group": 0, + "index": 45 + }, + { + "name": "Jondrette", + "group": 7, + "index": 46 + }, + { + "name": "Mme.Burgon", + "group": 7, + "index": 47 + }, + { + "name": "Gavroche", + "group": 8, + "index": 48 + }, + { + "name": "Gillenormand", + "group": 5, + "index": 49 + }, + { + "name": "Magnon", + "group": 5, + "index": 50 + }, + { + "name": "Mlle.Gillenormand", + "group": 5, + "index": 51 + }, + { + "name": "Mme.Pontmercy", + "group": 5, + "index": 52 + }, + { + "name": "Mlle.Vaubois", + "group": 5, + "index": 53 + }, + { + "name": "Lt.Gillenormand", + "group": 5, + "index": 54 + }, + { + "name": "Marius", + "group": 8, + "index": 55 + }, + { + "name": "BaronessT", + "group": 5, + "index": 56 + }, + { + "name": "Mabeuf", + "group": 8, + "index": 57 + }, + { + "name": "Enjolras", + "group": 8, + "index": 58 + }, + { + "name": "Combeferre", + "group": 8, + "index": 59 + }, + { + "name": "Prouvaire", + "group": 8, + "index": 60 + }, + { + "name": "Feuilly", + "group": 8, + "index": 61 + }, + { + "name": "Courfeyrac", + "group": 8, + "index": 62 + }, + { + "name": "Bahorel", + "group": 8, + "index": 63 + }, + { + "name": "Bossuet", + "group": 8, + "index": 64 + }, + { + "name": "Joly", + "group": 8, + "index": 65 + }, + { + "name": "Grantaire", + "group": 8, + "index": 66 + }, + { + "name": "MotherPlutarch", + "group": 9, + "index": 67 + }, + { + "name": "Gueulemer", + "group": 4, + "index": 68 + }, + { + "name": "Babet", + "group": 4, + "index": 69 + }, + { + "name": "Claquesous", + "group": 4, + "index": 70 + }, + { + "name": "Montparnasse", + "group": 4, + "index": 71 + }, + { + "name": "Toussaint", + "group": 5, + "index": 72 + }, + { + "name": "Child1", + "group": 10, + "index": 73 + }, + { + "name": "Child2", + "group": 10, + "index": 74 + }, + { + "name": "Brujon", + "group": 4, + "index": 75 + }, + { + "name": "Mme.Hucheloup", + "group": 8, + "index": 76 + } + ], + "links": [ + { + "source": 1, + "target": 0, + "value": 1 + }, + { + "source": 2, + "target": 0, + "value": 8 + }, + { + "source": 3, + "target": 0, + "value": 10 + }, + { + "source": 3, + "target": 2, + "value": 6 + }, + { + "source": 4, + "target": 0, + "value": 1 + }, + { + "source": 5, + "target": 0, + "value": 1 + }, + { + "source": 6, + "target": 0, + "value": 1 + }, + { + "source": 7, + "target": 0, + "value": 1 + }, + { + "source": 8, + "target": 0, + "value": 2 + }, + { + "source": 9, + "target": 0, + "value": 1 + }, + { + "source": 11, + "target": 10, + "value": 1 + }, + { + "source": 11, + "target": 3, + "value": 3 + }, + { + "source": 11, + "target": 2, + "value": 3 + }, + { + "source": 11, + "target": 0, + "value": 5 + }, + { + "source": 12, + "target": 11, + "value": 1 + }, + { + "source": 13, + "target": 11, + "value": 1 + }, + { + "source": 14, + "target": 11, + "value": 1 + }, + { + "source": 15, + "target": 11, + "value": 1 + }, + { + "source": 17, + "target": 16, + "value": 4 + }, + { + "source": 18, + "target": 16, + "value": 4 + }, + { + "source": 18, + "target": 17, + "value": 4 + }, + { + "source": 19, + "target": 16, + "value": 4 + }, + { + "source": 19, + "target": 17, + "value": 4 + }, + { + "source": 19, + "target": 18, + "value": 4 + }, + { + "source": 20, + "target": 16, + "value": 3 + }, + { + "source": 20, + "target": 17, + "value": 3 + }, + { + "source": 20, + "target": 18, + "value": 3 + }, + { + "source": 20, + "target": 19, + "value": 4 + }, + { + "source": 21, + "target": 16, + "value": 3 + }, + { + "source": 21, + "target": 17, + "value": 3 + }, + { + "source": 21, + "target": 18, + "value": 3 + }, + { + "source": 21, + "target": 19, + "value": 3 + }, + { + "source": 21, + "target": 20, + "value": 5 + }, + { + "source": 22, + "target": 16, + "value": 3 + }, + { + "source": 22, + "target": 17, + "value": 3 + }, + { + "source": 22, + "target": 18, + "value": 3 + }, + { + "source": 22, + "target": 19, + "value": 3 + }, + { + "source": 22, + "target": 20, + "value": 4 + }, + { + "source": 22, + "target": 21, + "value": 4 + }, + { + "source": 23, + "target": 16, + "value": 3 + }, + { + "source": 23, + "target": 17, + "value": 3 + }, + { + "source": 23, + "target": 18, + "value": 3 + }, + { + "source": 23, + "target": 19, + "value": 3 + }, + { + "source": 23, + "target": 20, + "value": 4 + }, + { + "source": 23, + "target": 21, + "value": 4 + }, + { + "source": 23, + "target": 22, + "value": 4 + }, + { + "source": 23, + "target": 12, + "value": 2 + }, + { + "source": 23, + "target": 11, + "value": 9 + }, + { + "source": 24, + "target": 23, + "value": 2 + }, + { + "source": 24, + "target": 11, + "value": 7 + }, + { + "source": 25, + "target": 24, + "value": 13 + }, + { + "source": 25, + "target": 23, + "value": 1 + }, + { + "source": 25, + "target": 11, + "value": 12 + }, + { + "source": 26, + "target": 24, + "value": 4 + }, + { + "source": 26, + "target": 11, + "value": 31 + }, + { + "source": 26, + "target": 16, + "value": 1 + }, + { + "source": 26, + "target": 25, + "value": 1 + }, + { + "source": 27, + "target": 11, + "value": 17 + }, + { + "source": 27, + "target": 23, + "value": 5 + }, + { + "source": 27, + "target": 25, + "value": 5 + }, + { + "source": 27, + "target": 24, + "value": 1 + }, + { + "source": 27, + "target": 26, + "value": 1 + }, + { + "source": 28, + "target": 11, + "value": 8 + }, + { + "source": 28, + "target": 27, + "value": 1 + }, + { + "source": 29, + "target": 23, + "value": 1 + }, + { + "source": 29, + "target": 27, + "value": 1 + }, + { + "source": 29, + "target": 11, + "value": 2 + }, + { + "source": 30, + "target": 23, + "value": 1 + }, + { + "source": 31, + "target": 30, + "value": 2 + }, + { + "source": 31, + "target": 11, + "value": 3 + }, + { + "source": 31, + "target": 23, + "value": 2 + }, + { + "source": 31, + "target": 27, + "value": 1 + }, + { + "source": 32, + "target": 11, + "value": 1 + }, + { + "source": 33, + "target": 11, + "value": 2 + }, + { + "source": 33, + "target": 27, + "value": 1 + }, + { + "source": 34, + "target": 11, + "value": 3 + }, + { + "source": 34, + "target": 29, + "value": 2 + }, + { + "source": 35, + "target": 11, + "value": 3 + }, + { + "source": 35, + "target": 34, + "value": 3 + }, + { + "source": 35, + "target": 29, + "value": 2 + }, + { + "source": 36, + "target": 34, + "value": 2 + }, + { + "source": 36, + "target": 35, + "value": 2 + }, + { + "source": 36, + "target": 11, + "value": 2 + }, + { + "source": 36, + "target": 29, + "value": 1 + }, + { + "source": 37, + "target": 34, + "value": 2 + }, + { + "source": 37, + "target": 35, + "value": 2 + }, + { + "source": 37, + "target": 36, + "value": 2 + }, + { + "source": 37, + "target": 11, + "value": 2 + }, + { + "source": 37, + "target": 29, + "value": 1 + }, + { + "source": 38, + "target": 34, + "value": 2 + }, + { + "source": 38, + "target": 35, + "value": 2 + }, + { + "source": 38, + "target": 36, + "value": 2 + }, + { + "source": 38, + "target": 37, + "value": 2 + }, + { + "source": 38, + "target": 11, + "value": 2 + }, + { + "source": 38, + "target": 29, + "value": 1 + }, + { + "source": 39, + "target": 25, + "value": 1 + }, + { + "source": 40, + "target": 25, + "value": 1 + }, + { + "source": 41, + "target": 24, + "value": 2 + }, + { + "source": 41, + "target": 25, + "value": 3 + }, + { + "source": 42, + "target": 41, + "value": 2 + }, + { + "source": 42, + "target": 25, + "value": 2 + }, + { + "source": 42, + "target": 24, + "value": 1 + }, + { + "source": 43, + "target": 11, + "value": 3 + }, + { + "source": 43, + "target": 26, + "value": 1 + }, + { + "source": 43, + "target": 27, + "value": 1 + }, + { + "source": 44, + "target": 28, + "value": 3 + }, + { + "source": 44, + "target": 11, + "value": 1 + }, + { + "source": 45, + "target": 28, + "value": 2 + }, + { + "source": 47, + "target": 46, + "value": 1 + }, + { + "source": 48, + "target": 47, + "value": 2 + }, + { + "source": 48, + "target": 25, + "value": 1 + }, + { + "source": 48, + "target": 27, + "value": 1 + }, + { + "source": 48, + "target": 11, + "value": 1 + }, + { + "source": 49, + "target": 26, + "value": 3 + }, + { + "source": 49, + "target": 11, + "value": 2 + }, + { + "source": 50, + "target": 49, + "value": 1 + }, + { + "source": 50, + "target": 24, + "value": 1 + }, + { + "source": 51, + "target": 49, + "value": 9 + }, + { + "source": 51, + "target": 26, + "value": 2 + }, + { + "source": 51, + "target": 11, + "value": 2 + }, + { + "source": 52, + "target": 51, + "value": 1 + }, + { + "source": 52, + "target": 39, + "value": 1 + }, + { + "source": 53, + "target": 51, + "value": 1 + }, + { + "source": 54, + "target": 51, + "value": 2 + }, + { + "source": 54, + "target": 49, + "value": 1 + }, + { + "source": 54, + "target": 26, + "value": 1 + }, + { + "source": 55, + "target": 51, + "value": 6 + }, + { + "source": 55, + "target": 49, + "value": 12 + }, + { + "source": 55, + "target": 39, + "value": 1 + }, + { + "source": 55, + "target": 54, + "value": 1 + }, + { + "source": 55, + "target": 26, + "value": 21 + }, + { + "source": 55, + "target": 11, + "value": 19 + }, + { + "source": 55, + "target": 16, + "value": 1 + }, + { + "source": 55, + "target": 25, + "value": 2 + }, + { + "source": 55, + "target": 41, + "value": 5 + }, + { + "source": 55, + "target": 48, + "value": 4 + }, + { + "source": 56, + "target": 49, + "value": 1 + }, + { + "source": 56, + "target": 55, + "value": 1 + }, + { + "source": 57, + "target": 55, + "value": 1 + }, + { + "source": 57, + "target": 41, + "value": 1 + }, + { + "source": 57, + "target": 48, + "value": 1 + }, + { + "source": 58, + "target": 55, + "value": 7 + }, + { + "source": 58, + "target": 48, + "value": 7 + }, + { + "source": 58, + "target": 27, + "value": 6 + }, + { + "source": 58, + "target": 57, + "value": 1 + }, + { + "source": 58, + "target": 11, + "value": 4 + }, + { + "source": 59, + "target": 58, + "value": 15 + }, + { + "source": 59, + "target": 55, + "value": 5 + }, + { + "source": 59, + "target": 48, + "value": 6 + }, + { + "source": 59, + "target": 57, + "value": 2 + }, + { + "source": 60, + "target": 48, + "value": 1 + }, + { + "source": 60, + "target": 58, + "value": 4 + }, + { + "source": 60, + "target": 59, + "value": 2 + }, + { + "source": 61, + "target": 48, + "value": 2 + }, + { + "source": 61, + "target": 58, + "value": 6 + }, + { + "source": 61, + "target": 60, + "value": 2 + }, + { + "source": 61, + "target": 59, + "value": 5 + }, + { + "source": 61, + "target": 57, + "value": 1 + }, + { + "source": 61, + "target": 55, + "value": 1 + }, + { + "source": 62, + "target": 55, + "value": 9 + }, + { + "source": 62, + "target": 58, + "value": 17 + }, + { + "source": 62, + "target": 59, + "value": 13 + }, + { + "source": 62, + "target": 48, + "value": 7 + }, + { + "source": 62, + "target": 57, + "value": 2 + }, + { + "source": 62, + "target": 41, + "value": 1 + }, + { + "source": 62, + "target": 61, + "value": 6 + }, + { + "source": 62, + "target": 60, + "value": 3 + }, + { + "source": 63, + "target": 59, + "value": 5 + }, + { + "source": 63, + "target": 48, + "value": 5 + }, + { + "source": 63, + "target": 62, + "value": 6 + }, + { + "source": 63, + "target": 57, + "value": 2 + }, + { + "source": 63, + "target": 58, + "value": 4 + }, + { + "source": 63, + "target": 61, + "value": 3 + }, + { + "source": 63, + "target": 60, + "value": 2 + }, + { + "source": 63, + "target": 55, + "value": 1 + }, + { + "source": 64, + "target": 55, + "value": 5 + }, + { + "source": 64, + "target": 62, + "value": 12 + }, + { + "source": 64, + "target": 48, + "value": 5 + }, + { + "source": 64, + "target": 63, + "value": 4 + }, + { + "source": 64, + "target": 58, + "value": 10 + }, + { + "source": 64, + "target": 61, + "value": 6 + }, + { + "source": 64, + "target": 60, + "value": 2 + }, + { + "source": 64, + "target": 59, + "value": 9 + }, + { + "source": 64, + "target": 57, + "value": 1 + }, + { + "source": 64, + "target": 11, + "value": 1 + }, + { + "source": 65, + "target": 63, + "value": 5 + }, + { + "source": 65, + "target": 64, + "value": 7 + }, + { + "source": 65, + "target": 48, + "value": 3 + }, + { + "source": 65, + "target": 62, + "value": 5 + }, + { + "source": 65, + "target": 58, + "value": 5 + }, + { + "source": 65, + "target": 61, + "value": 5 + }, + { + "source": 65, + "target": 60, + "value": 2 + }, + { + "source": 65, + "target": 59, + "value": 5 + }, + { + "source": 65, + "target": 57, + "value": 1 + }, + { + "source": 65, + "target": 55, + "value": 2 + }, + { + "source": 66, + "target": 64, + "value": 3 + }, + { + "source": 66, + "target": 58, + "value": 3 + }, + { + "source": 66, + "target": 59, + "value": 1 + }, + { + "source": 66, + "target": 62, + "value": 2 + }, + { + "source": 66, + "target": 65, + "value": 2 + }, + { + "source": 66, + "target": 48, + "value": 1 + }, + { + "source": 66, + "target": 63, + "value": 1 + }, + { + "source": 66, + "target": 61, + "value": 1 + }, + { + "source": 66, + "target": 60, + "value": 1 + }, + { + "source": 67, + "target": 57, + "value": 3 + }, + { + "source": 68, + "target": 25, + "value": 5 + }, + { + "source": 68, + "target": 11, + "value": 1 + }, + { + "source": 68, + "target": 24, + "value": 1 + }, + { + "source": 68, + "target": 27, + "value": 1 + }, + { + "source": 68, + "target": 48, + "value": 1 + }, + { + "source": 68, + "target": 41, + "value": 1 + }, + { + "source": 69, + "target": 25, + "value": 6 + }, + { + "source": 69, + "target": 68, + "value": 6 + }, + { + "source": 69, + "target": 11, + "value": 1 + }, + { + "source": 69, + "target": 24, + "value": 1 + }, + { + "source": 69, + "target": 27, + "value": 2 + }, + { + "source": 69, + "target": 48, + "value": 1 + }, + { + "source": 69, + "target": 41, + "value": 1 + }, + { + "source": 70, + "target": 25, + "value": 4 + }, + { + "source": 70, + "target": 69, + "value": 4 + }, + { + "source": 70, + "target": 68, + "value": 4 + }, + { + "source": 70, + "target": 11, + "value": 1 + }, + { + "source": 70, + "target": 24, + "value": 1 + }, + { + "source": 70, + "target": 27, + "value": 1 + }, + { + "source": 70, + "target": 41, + "value": 1 + }, + { + "source": 70, + "target": 58, + "value": 1 + }, + { + "source": 71, + "target": 27, + "value": 1 + }, + { + "source": 71, + "target": 69, + "value": 2 + }, + { + "source": 71, + "target": 68, + "value": 2 + }, + { + "source": 71, + "target": 70, + "value": 2 + }, + { + "source": 71, + "target": 11, + "value": 1 + }, + { + "source": 71, + "target": 48, + "value": 1 + }, + { + "source": 71, + "target": 41, + "value": 1 + }, + { + "source": 71, + "target": 25, + "value": 1 + }, + { + "source": 72, + "target": 26, + "value": 2 + }, + { + "source": 72, + "target": 27, + "value": 1 + }, + { + "source": 72, + "target": 11, + "value": 1 + }, + { + "source": 73, + "target": 48, + "value": 2 + }, + { + "source": 74, + "target": 48, + "value": 2 + }, + { + "source": 74, + "target": 73, + "value": 3 + }, + { + "source": 75, + "target": 69, + "value": 3 + }, + { + "source": 75, + "target": 68, + "value": 3 + }, + { + "source": 75, + "target": 25, + "value": 3 + }, + { + "source": 75, + "target": 48, + "value": 1 + }, + { + "source": 75, + "target": 41, + "value": 1 + }, + { + "source": 75, + "target": 70, + "value": 1 + }, + { + "source": 75, + "target": 71, + "value": 1 + }, + { + "source": 76, + "target": 64, + "value": 1 + }, + { + "source": 76, + "target": 65, + "value": 1 + }, + { + "source": 76, + "target": 66, + "value": 1 + }, + { + "source": 76, + "target": 63, + "value": 1 + }, + { + "source": 76, + "target": 62, + "value": 1 + }, + { + "source": 76, + "target": 48, + "value": 1 + }, + { + "source": 76, + "target": 58, + "value": 1 + } + ] +} \ No newline at end of file diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql new file mode 100644 index 0000000..257647a --- /dev/null +++ b/resources/sql/queries.sql @@ -0,0 +1,34 @@ +-- :name create-user! :! :n +-- :doc creates a new user record +INSERT INTO users +(name, gender) +VALUES (:name, :gender) + +-- :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-users :? :* +-- :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 + + +-- :name create-relation! :! :n +-- :doc creates a new relation record +INSERT INTO relations +(from_id, to_id) +VALUES (:from_id, :to_id) + +-- :name get-relations :? :* +-- :doc retrieves all relations +SELECT * FROM relations +JOIN users u_from on relations.from_id = u_from.id +JOIN users u_to on relations.to_id = u_to.id \ No newline at end of file diff --git a/src/clj/cat/config.clj b/src/clj/cat/config.clj new file mode 100644 index 0000000..572634b --- /dev/null +++ b/src/clj/cat/config.clj @@ -0,0 +1,12 @@ +(ns cat.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/cat/core.clj b/src/clj/cat/core.clj new file mode 100644 index 0000000..7b75d5f --- /dev/null +++ b/src/clj/cat/core.clj @@ -0,0 +1,66 @@ +(ns cat.core + (:require [cat.handler :as handler] + [cat.nrepl :as nrepl] + [luminus.http-server :as http] + [luminus-migrations.core :as migrations] + [cat.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 :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 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] + (mount/start #'cat.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))) + diff --git a/src/clj/cat/db/core.clj b/src/clj/cat/db/core.clj new file mode 100644 index 0000000..404b9c3 --- /dev/null +++ b/src/clj/cat/db/core.clj @@ -0,0 +1,85 @@ +(ns cat.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] + [cat.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))) + diff --git a/src/clj/cat/handler.clj b/src/clj/cat/handler.clj new file mode 100644 index 0000000..cc6d3c3 --- /dev/null +++ b/src/clj/cat/handler.clj @@ -0,0 +1,28 @@ +(ns cat.handler + (:require [cat.middleware :as middleware] + [cat.layout :refer [error-page]] + [cat.routes.home :refer [home-routes]] + [cat.routes.oauth :refer [oauth-routes]] + [compojure.core :refer [routes wrap-routes]] + [ring.util.http-response :as response] + [compojure.route :as route] + [cat.env :refer [defaults]] + [mount.core :as mount])) + +(mount/defstate init-app + :start ((or (:init defaults) identity)) + :stop ((or (:stop defaults) identity))) + +(mount/defstate app + :start + (middleware/wrap-base + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + #'oauth-routes + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"})))))) + diff --git a/src/clj/cat/layout.clj b/src/clj/cat/layout.clj new file mode 100644 index 0000000..a8200f9 --- /dev/null +++ b/src/clj/cat/layout.clj @@ -0,0 +1,37 @@ +(ns cat.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*]])) + + +(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" + [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)}) diff --git a/src/clj/cat/middleware.clj b/src/clj/cat/middleware.clj new file mode 100644 index 0000000..942e2f1 --- /dev/null +++ b/src/clj/cat/middleware.clj @@ -0,0 +1,73 @@ +(ns cat.middleware + (:require [cat.env :refer [defaults]] + [cheshire.generate :as cheshire] + [cognitect.transit :as transit] + [clojure.tools.logging :as log] + [cat.layout :refer [error-page]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.webjars :refer [wrap-webjars]] + [cat.middleware.formats :as formats] + [muuntaja.middleware :refer [wrap-format wrap-params]] + [cat.config :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]] + [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] + [buddy.auth.accessrules :refer [restrict]] + [buddy.auth :refer [authenticated?]] + [buddy.auth.backends.session :refer [session-backend]]) + (:import)) + + +(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 on-error [request response] + (error-page + {:status 403 + :title (str "Access to " (:uri request) " is not authorized")})) + +(defn wrap-restricted [handler] + (restrict handler {:handler authenticated? + :on-error on-error})) + +(defn wrap-auth [handler] + (let [backend (session-backend)] + (-> handler + (wrap-authentication backend) + (wrap-authorization backend)))) + +(defn wrap-base [handler] + (-> ((:middleware defaults) handler) + wrap-auth + wrap-webjars + wrap-flash + (wrap-session {:cookie-attrs {:http-only true}}) + (wrap-defaults + (-> site-defaults + (assoc-in [:security :anti-forgery] false) + (dissoc :session))) + wrap-internal-error)) diff --git a/src/clj/cat/middleware/formats.clj b/src/clj/cat/middleware/formats.clj new file mode 100644 index 0000000..c8950e3 --- /dev/null +++ b/src/clj/cat/middleware/formats.clj @@ -0,0 +1,14 @@ +(ns cat.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))))) diff --git a/src/clj/cat/nrepl.clj b/src/clj/cat/nrepl.clj new file mode 100644 index 0000000..82af366 --- /dev/null +++ b/src/clj/cat/nrepl.clj @@ -0,0 +1,26 @@ +(ns cat.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")) diff --git a/src/clj/cat/oauth.clj b/src/clj/cat/oauth.clj new file mode 100644 index 0000000..bfb67d3 --- /dev/null +++ b/src/clj/cat/oauth.clj @@ -0,0 +1,35 @@ +(ns cat.oauth + (:require [cat.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))) diff --git a/src/clj/cat/routes/home.clj b/src/clj/cat/routes/home.clj new file mode 100644 index 0000000..b0999bc --- /dev/null +++ b/src/clj/cat/routes/home.clj @@ -0,0 +1,88 @@ +(ns cat.routes.home + (:require [cat.layout :as layout] + [cat.db.core :refer [*db*] :as db] + [compojure.core :refer [defroutes GET POST]] + [ring.util.http-response :as response] + [clojure.java.io :as io] + [struct.core :as st] + [clojure.edn :as edn] + [clojure.tools.logging :as log] + [clojure.data.json :as json])) + +(def user-schema + [[:name st/required st/string] + [:gender st/string]]) + +(def relation-schema + [[:from_id st/required st/integer-str] + [:to_id st/required st/integer-str]]) + +(defn home-page [params] + (layout/render "home.html" params)) + +(defn get-relations [] + (map + (fn [relation] (select-keys relation [:name :name_2])) + (db/get-relations))) + +(defn get-users [] + (db/get-users)) + +(defroutes home-routes + (GET "/" [] + (let [users (get-users) + relations (get-relations)] + (home-page {:relations relations :users users}))) + ;(GET "/docs" [] + ; (-> (response/ok (-> "docs/docs.md" io/resource slurp)) + ; (response/header "Content-Type" "text/plain; charset=utf-8"))) + (GET "/relations" [] + (let [] + (response/ok {}))) + (GET "/relations_zeroed" [] + (let [users (db/get-users) + relations (db/get-relations) + used-node-ids (set (flatten (map (fn [ln] [(:from_id ln) (:to_id ln)]) relations))) + filtered-users (filter (fn [{id :id}] (contains? used-node-ids id)) users) + id-index-map (:map (reduce (fn [{map :map idx :index} usr] + {:map (assoc map (:id usr) idx) + :index (inc idx)}) + {:map {} :index 0} + filtered-users)) + rels-indexed (map (fn [{src :from_id target :to_id}] + {:source (get id-index-map src) + :target (get id-index-map target) + :value (+ 20 (rand-int 30))}) + relations) + nodes-indexed (->> filtered-users + (map (fn [usr] + (-> usr + (dissoc :gender :id) + (assoc :index (get id-index-map (:id usr))) + (assoc :group (rand-int 2))))))] + (response/ok {:nodes nodes-indexed + :links rels-indexed}))) + (POST "/relations" req + (let [data (:params req) [err result] (st/validate data relation-schema)] + (log/info "Post to " (:uri req)) + (if (nil? err) + (do + (db/create-relation! result) + (response/no-content)) + (do + (response/bad-request "Incorrect input"))))) + (POST "/users" req + (let [data (:params req)] + (log/info "Post to " (:uri req)) + (println data) + (if (st/valid? data user-schema) + (do + (db/create-user! data) + (response/no-content)) + (do + (response/bad-request "Incorrect input")))))) + + + + + diff --git a/src/clj/cat/routes/oauth.clj b/src/clj/cat/routes/oauth.clj new file mode 100644 index 0000000..7c98d0b --- /dev/null +++ b/src/clj/cat/routes/oauth.clj @@ -0,0 +1,34 @@ +(ns cat.routes.oauth + (:require [ring.util.http-response :refer [ok found]] + [compojure.core :refer [defroutes GET]] + [clojure.java.io :as io] + [cat.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)))))) + + +(defroutes oauth-routes + (GET "/oauth/oauth-init" req (oauth-init req)) + (GET "/oauth/oauth-callback" [& req_token :as req] (oauth-callback req))) + diff --git a/src/cljc/cat/validation.cljc b/src/cljc/cat/validation.cljc new file mode 100644 index 0000000..1205d4b --- /dev/null +++ b/src/cljc/cat/validation.cljc @@ -0,0 +1,2 @@ +(ns cat.validation + (:require [struct.core :as st])) diff --git a/src/cljs/cat/core.cljs b/src/cljs/cat/core.cljs new file mode 100644 index 0000000..a260554 --- /dev/null +++ b/src/cljs/cat/core.cljs @@ -0,0 +1,159 @@ +(ns cat.core + (:require [vega-tools.core :refer [validate-and-parse]] + [promesa.core :as p])) + + +;(defn ^:export main [] +; (println "This is the main function.") +; (let [spec {:width 200 :height 200 +; :marks [{:type "symbol" +; :properties {:enter {:size {:value 1000} +; :x {:value 100} +; :y {:value 100} +; :shape {:value "circle"} +; :stroke {:value "red"}}}}]}] +; (-> (validate-and-parse spec) +; (p/catch #(js/alert (str "Unable to parse spec:\n\n" %))) +; (p/then #(-> (% {:el (js/document.getElementById "#chart")}) +; (.update)))))) + + +;(defn on-js-reload [] +; (println "This is the on-js-reload function.") +; (main)) + +(defn mount-components [] + (let [spec {:width 700 :height 500 + :signals [{:name "cx"}] + :marks [{:name "nodes" + :type "symbol" + :properties {:enter {:size {:value 1000} + :x {:value 100} + :y {:value 100} + :shape {:value "circle"} + :stroke {:value "red"}}}}]} + nodespec {;:$schema "https://vega.github.io/schema/vega/v4.json", + :width 700, + :height 500, + :padding 0, + :autosize "none", + :signals [{:name "cx", :update "width / 2"} + {:name "cy", :update "height / 2"} + {:name "nodeRadius", :value 8, + :bind {:input "range", + :min 1, + :max 50, + :step 1}} + {:name "nodeCharge", :value -30, + :bind {:input "range", + :min -100, + :max 10, + :step 1}} + {:name "linkDistance", :value 30, + :bind {:input "range", + :min 5, + :max 100, + :step 1}} + {:name "static", :value true, + :bind {:input "checkbox"}} + ;{:description + ; "State variable for active node fix status.", + ; :name "fix", :value false, + ; :on [{:events "symbol:mouseout[!event.buttons], window:mouseup", + ; :update "false"} + ; {:events "symbol:mouseover", + ; :update "fix || true"} + ; {:events "[symbol:mousedown, window:mouseup] > window:mousemove!", + ; :update "xy()", + ; :force true}]} + ;{:description "Graph node most recently interacted with.", + ; :name "node", :value nil, + ; :on [{:events "symbol:mouseover", + ; :update "fix === true ? item() : node"}]} + {:description "Flag to restart Force simulation upon data changes.", + :name "restart", + :value false, + :on [{:events {:signal "fix"}, + :update "fix && fix.length"}]}], + :data [{:name "node-data" + :url "/relations" + :format {:type "json", :property "nodes"} + } + {:name "link-data" + :url "/relations" + :format {:type "json", :property "links"} + :transform [{:type "lookup" + :fields ["name" "name_2"] + :as ["source" "target"]}] + }], + ;:scales + ; [{:name "color", + ; :type "ordinal", + ; :domain {:data "node-data", :field "name"}, + ; :range {:scheme "category20c"}}], + + :marks + [{:name "nodes", + :type "symbol", + :zindex 1, + :from {:data "node-data"}, + :on + [{:trigger "fix", + :modify "node", + :values + "fix === true ? {fx: node.x, fy: node.y} : {fx: fix[0], fy: fix[1]}"} + {:trigger "!fix", + :modify "node", + :values "{fx: null, fy: null}"}], + ;:encode + ; {:enter + ; {:fill {:scale "color", :field "group"}, + ; :stroke {:value "white"}}, + ; :update + ; {:size + ; {:signal "2 * nodeRadius * nodeRadius"}, + ; :cursor {:value "pointer"}}}, + :transform + [{:type "force", + :iterations 300, + :restart {:signal "restart"}, + :static {:signal "static"}, + :signal "force", + :forces [{:force "center", + :x {:signal "cx"}, + :y {:signal "cy"}} + {:force "collide", + :radius {:signal "nodeRadius"}} + {:force "nbody", + :strength {:signal "nodeCharge"}} + {:force "link", + :links "link-data", + :distance {:signal "linkDistance"}}]}]} + {:type "path", + :interactive false, + :from {:data "link-data"}, + :encode {:update + {:stroke {:value "#ccc"}, + :strokeWidth {:value 0.5}}}, + :transform + [{:type "linkpath", + :require {:signal "force"}, + :shape "line", + :sourceX "datum.source.x", :sourceY "datum.source.y", + :targetX "datum.target.x", :targetY "datum.target.y"}]}]}] + + ;(-> (validate-and-parse nodespec) + ; (p/catch #(js/alert (str "Unable to parse spec:\n\n" %))) + ; (p/then #(-> (% {:el (js/document.getElementById "chart")}) + ; (.update)))) + + (let [content (js/document.getElementById "app")] + (while (.hasChildNodes content) + (.removeChild content (.-lastChild content))) + (.appendChild content (js/document.createTextNode "Welcome to cat"))))) + +(defn init! [] + (mount-components)) +;(main)) + + diff --git a/test/clj/cat/test/db/core.clj b/test/clj/cat/test/db/core.clj new file mode 100644 index 0000000..b8dbc0a --- /dev/null +++ b/test/clj/cat/test/db/core.clj @@ -0,0 +1,36 @@ +(ns cat.test.db.core + (:require [cat.db.core :refer [*db*] :as db] + [luminus-migrations.core :as migrations] + [clojure.test :refer :all] + [clojure.java.jdbc :as jdbc] + [cat.config :refer [env]] + [mount.core :as mount])) + +(use-fixtures + :once + (fn [f] + (mount/start + #'cat.config/env + #'cat.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/cat/test/handler.clj b/test/clj/cat/test/handler.clj new file mode 100644 index 0000000..b78e6fd --- /dev/null +++ b/test/clj/cat/test/handler.clj @@ -0,0 +1,26 @@ +(ns cat.test.handler + (:require [clojure.test :refer :all] + [ring.mock.request :refer :all] + [cat.handler :refer :all] + [cat.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 #'cat.config/env + #'cat.handler/app) + (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)))))) diff --git a/test/cljs/cat/core_test.cljs b/test/cljs/cat/core_test.cljs new file mode 100644 index 0000000..bc467eb --- /dev/null +++ b/test/cljs/cat/core_test.cljs @@ -0,0 +1,8 @@ +(ns cat.core-test + (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] + [pjstadig.humane-test-output] + [cat.core :as rc])) + +(deftest test-home + (is (= true true))) + diff --git a/test/cljs/cat/doo_runner.cljs b/test/cljs/cat/doo_runner.cljs new file mode 100644 index 0000000..7089890 --- /dev/null +++ b/test/cljs/cat/doo_runner.cljs @@ -0,0 +1,6 @@ +(ns cat.doo-runner + (:require [doo.runner :refer-macros [doo-tests]] + [cat.core-test])) + +(doo-tests 'cat.core-test) +