diff --git a/.gitignore b/.gitignore index 7f97479..faf3ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ pom.xml.asc /.nrepl-port .hgignore .hg/ +.idea resources/auto/ @@ -17,3 +18,5 @@ generated/resources/sql/ generated/resources/templates/auto/ generated/src/clj/youyesyet/routes/ + +*.iml diff --git a/src/adl/to_selmer_templates.clj b/src/adl/to_selmer_templates.clj index 3e6767c..4aa5eb9 100644 --- a/src/adl/to_selmer_templates.clj +++ b/src/adl/to_selmer_templates.clj @@ -39,7 +39,7 @@ {:tag :div :attrs {:class "big-link-container"} :content - [{:tag :a :attrs {:href url :class "big-link"} + [{:tag :a :attrs {:href (str "{{servlet-context}}/" url) :class "big-link"} :content (if (vector? content) content @@ -51,7 +51,7 @@ {:tag :div :attrs {:class "back-link-container"} :content - [{:tag :a :attrs {:href url} + [{:tag :a :attrs {:href (str "{{servlet-context}}/" url)} :content (if (vector? content) content @@ -60,18 +60,20 @@ (defn emit-content ([content] - (cond - (nil? content) - nil - (string? content) - content - (and (map? content) (:tag content)) - (with-out-str - (x/emit-element content)) - (seq? content) - (map emit-content content) - true - (str ""))) + (try + (cond + (nil? content) + nil + (string? content) + content + (and (map? content) (:tag content)) + (with-out-str + (x/emit-element content)) + (seq? content) + (map emit-content content) + true + (str "")) + (catch Exception _ (str "")))) ([filename application k] (emit-content filename nil nil application k)) ([filename spec entity application k] @@ -94,15 +96,6 @@ content)) "{% endblock %}")))))) -;; {:tag :div, :content -;; [{:tag :div, :attrs {:class big-link-container}, :content -;; [{:tag :a, :attrs {:id next-selector, :role button, :class big-link}, -;; :content [Next]}]} -;; [{% ifmemberof admin %} -;; {:tag :div, :attrs {:class big-link-container}, :content -;; [{:tag :a, :attrs {:href form-electors-Elector, :class big-link}, :content [Add a new Elector]}]} -;; {% endifmemberof %}]]} - (defn file-header "Generate a header for a template file with this `filename` for this `spec` @@ -155,7 +148,8 @@ (= (:tag %) :prompt) (= (:locale :attrs %) *locale*)))) (:name (:attrs field-or-property)) - (:property (:attrs field-or-property)))))) + (:property (:attrs field-or-property)) + "Missing prompt")))) (defn csrf-widget @@ -165,15 +159,15 @@ (defn compose-if-member-of-tag - [writable? & elts] + [privilege & elts] (let [all-permissions (distinct (apply find-permissions elts)) permissions (map s/lower-case - (if - writable? - (writable-by all-permissions) - (visible-to all-permissions)))] + (case privilege + :writeable (writeable-by all-permissions) + :editable (writeable-by all-permissions true) + :readable (visible-to all-permissions)))] (s/join " " (flatten @@ -184,12 +178,12 @@ (defn wrap-in-if-member-of - "Wrap this `content` in an if-member-of tag; if `writable?` is true, - allow those groups by whom it is writable, else those by whom it is + "Wrap this `content` in an if-member-of tag; if `writeable?` is true, + allow those groups by whom it is writeable, else those by whom it is readable. `context` should be a sequence of adl elements from which permissions may be obtained." - [content writable? & context] - [(apply compose-if-member-of-tag (cons writable? context)) + [content privilege & context] + [(apply compose-if-member-of-tag (cons privilege context)) content "{% endifmemberof %}"]) @@ -211,7 +205,7 @@ :class "action-safe" :type "submit" :value (str "Save!")}}]} - true + :editable entity application)) @@ -233,7 +227,7 @@ :class "action-dangerous" :type "submit" :value (str "Delete!")}}]} - true + :editable entity application)) @@ -275,9 +269,8 @@ (:type (:attrs typedef)) (:type (:attrs property)))] (if - (= (-> property :attrs :distinct) "system") - "hidden" ;; <- this is slightly wrong. There are some circumstances in which - ;; system-distinct properties might be user-editable + (and (= (-> property :attrs :distinct) "system") (= (-> property :attrs :immutable) "true")) + "hidden" (case t ("integer" "real" "money") "number" ("uploadable" "image") "file" @@ -286,8 +279,8 @@ "date" "date" "time" "time" "text" "text-area" - "string" ;; default - ))))) + ;; default + "string"))))) (defn select-widget @@ -337,30 +330,49 @@ :content (apply vector (get-options property form entity application))}))))})) +(defn compose-readable-or-not-authorised + [p f e a w] + (list + (compose-if-member-of-tag :readable p e a) + {:tag :span + :attrs {:id w + :name w + :class "pseudo-widget disabled"} + :content [(str "{{record." w "}}")]} + "{% else %}" + {:tag :span + :attrs {:id w + :name w + :class "pseudo-widget not-authorised"} + :content [(str "You are not permitted to view " w " of " (:name (:attrs e)))]} + "{% endifmemberof %}" + )) + + (defn compose-widget-para [p f e a w content] {:tag :p :attrs {:class "widget"} - :content [{:tag :label - :attrs {:for w} - :content [(prompt p f e a)]} - (compose-if-member-of-tag true p e a) - content - "{% else %}" - (compose-if-member-of-tag false p e a) - {:tag :span - :attrs {:id w - :name w - :class "pseudo-widget disabled"} - :content [(str "{{record." w "}}")]} - "{% else %}" - {:tag :span - :attrs {:id w - :name w - :class "pseudo-widget not-authorised"} - :content [(str "You are not permitted to view " w " of " (:name (:attrs e)))]} - "{% endifmemberof %}" - "{% endifmemberof %}"]}) + :content (apply + vector + (flatten + (list + {:tag :label + :attrs {:for w} + :content [(prompt p f e a)]} + (str "{% if {{record." (-> p :attrs :name) "}} %}") + (compose-if-member-of-tag :editable p e a) + content + "{% else %}" + (compose-readable-or-not-authorised p f e a w) + "{% endifmemberof %}" + "{% else %}" + (compose-if-member-of-tag :writeable p e a) + content + "{% else %}" + (compose-readable-or-not-authorised p f e a w) + "{% endifmemberof %}" + "{% endif %}")))}) (defn widget @@ -375,58 +387,62 @@ property (if (= (:tag field-or-property) :property) field-or-property - (child-with-tag entity - :property - #(= (:name (:attrs %)) - (:property (:attrs field-or-property))))) + (first + (filter + #(= (:name (:attrs %)) + (:property (:attrs field-or-property))) + (descendants-with-tag entity + :property)))) permissions (find-permissions field-or-property property form entity application) typedef (typedef property application) w-type (widget-type property application typedef) visible-to (visible-to permissions) - ;; if the form isn't actually a form, no widget is writable. - writable-by (if (= (:tag form) :form) (writable-by permissions))] - (case w-type - "hidden" - {:tag :input - :attrs {:id widget-name - :name widget-name - :type "hidden" - :value (str "{{record." widget-name "}}")}} - "select" - (compose-widget-para field-or-property form entity application widget-name - (select-widget property form entity application)) - "text-area" - (compose-widget-para - field-or-property form entity application widget-name - {:tag :textarea - :attrs {:rows "8" :cols "60" :id widget-name :name widget-name} - :content [(str "{{record." widget-name "}}")]}) - ;; all others - (compose-widget-para - field-or-property form entity application widget-name + ;; if the form isn't actually a form, no widget is writeable. + writeable-by (if (= (:tag form) :form) (writeable-by permissions))] + (if + property + (case w-type + "hidden" {:tag :input - :attrs (merge - {:id widget-name - :name widget-name - :type w-type - :value (str "{{record." widget-name "}}") - :maxlength (:size (:attrs property)) - :size (cond - (nil? (:size (:attrs property))) - "16" - (try - (> (read-string - (:size (:attrs property))) 60) - (catch Exception _ false)) - "60" - true - (:size (:attrs property)))} - (if - (:minimum (:attrs typedef)) - {:min (:minimum (:attrs typedef))}) - (if - (:maximum (:attrs typedef)) - {:max (:maximum (:attrs typedef))}))})))) + :attrs {:id widget-name + :name widget-name + :type "hidden" + :value (str "{{record." widget-name "}}")}} + "select" + (compose-widget-para property form entity application widget-name + (select-widget property form entity application)) + "text-area" + (compose-widget-para + property form entity application widget-name + {:tag :textarea + :attrs {:rows "8" :cols "60" :id widget-name :name widget-name} + :content [(str "{{record." widget-name "}}")]}) + ;; all others + (compose-widget-para + property form entity application widget-name + {:tag :input + :attrs (merge + {:id widget-name + :name widget-name + :type w-type + :value (str "{{record." widget-name "}}") + :maxlength (:size (:attrs property)) + :size (cond + (nil? (:size (:attrs property))) + "16" + (try + (> (read-string + (:size (:attrs property))) 60) + (catch Exception _ false)) + "60" + true + (:size (:attrs property)))} + (if + (:minimum (:attrs typedef)) + {:min (:minimum (:attrs typedef))}) + (if + (:maximum (:attrs typedef)) + {:max (:maximum (:attrs typedef))}))}))))) (defn embed-script-fragment @@ -446,14 +462,16 @@ (defn compose-form-content [form entity application] - {:content - {:tag :div - :attrs {:id "content" :class "edit"} - :content - [{:tag :form - :attrs {:action (str "{{servlet-context}}/" (editor-name entity application)) - :method "POST"} - :content (flatten + {:content + {:tag :div + :attrs {:id "content" :class "edit"} + :content + [{:tag :form + :attrs {:action (str "{{servlet-context}}/" (editor-name entity application)) + :method "POST"} + :content (apply + vector + (flatten (list (csrf-widget) (map @@ -469,7 +487,7 @@ (= (:distict (:attrs property)) :system)) (children-with-tag form :field))) (save-widget form entity application) - (delete-widget form entity application)))}]}}) + (delete-widget form entity application))))}]}}) (defn compose-form-extra-head @@ -587,6 +605,7 @@ (defn edit-link [entity application parameters] (str + "{{servlet-context}}/" (editor-name entity application) "?" (s/join @@ -677,7 +696,7 @@ :content ["Next"]}]} (wrap-in-if-member-of (big-link (str "Add a new " (pretty-name entity)) (editor-name entity application)) - true + :writeable entity application)))))} :content @@ -761,9 +780,9 @@ {:tag :dt :content [{:tag :a - :attrs {:href (path-part :list entity application)} + :attrs {:href (str "{{servlet-context}}/" (path-part :list entity application))} :content [(pretty-name entity)]}]} - false + :readable entity application)) @@ -781,37 +800,37 @@ :tag :p :content (:content d))) (children-with-tag entity :documentation)))} - false + :readable entity application)) - (defn application-to-template - [application] - (let - [first-class-entities - (sort-by - #(:name (:attrs %)) - (filter - #(children-with-tag % :list) - (children-with-tag application :entity)))] - {:application-index - {:content - {:tag :dl - :attrs {:class "index"} - :content - (apply - vector - (remove - nil? - (flatten - (interleave - (map - #(emit-entity-dt % application) - first-class-entities) - (map - #(emit-entity-dd % application) - first-class-entities)))))}}})) +(defn application-to-template + [application] + (let + [first-class-entities + (sort-by + #(:name (:attrs %)) + (filter + #(children-with-tag % :list) + (children-with-tag application :entity)))] + {:application-index + {:content + {:tag :dl + :attrs {:class "index"} + :content + (apply + vector + (remove + nil? + (flatten + (interleave + (map + #(emit-entity-dt % application) + first-class-entities) + (map + #(emit-entity-dd % application) + first-class-entities)))))}}})) (defn write-template-file diff --git a/src/adl/to_swagger.clj b/src/adl/to_swagger.clj new file mode 100644 index 0000000..b069d80 --- /dev/null +++ b/src/adl/to_swagger.clj @@ -0,0 +1,61 @@ +(ns ^{:doc "Application Description Language: generate swagger routes." + :author "Simon Brooke"} + adl.to-swagger + (:require [adl-support.utils :refer :all] + [adl.to-hugsql-queries :refer [queries]] + [clj-time.core :as t] + [clj-time.format :as f] + [clojure.java.io :refer [file make-parents writer]] + [clojure.pprint :refer [pprint]] + [clojure.string :as s] + [clojure.xml :as x])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; +;;;; adl.to-swagger: generate swagger routes. +;;;; +;;;; This program is free software; you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License +;;;; as published by the Free Software Foundation; either version 2 +;;;; of the License, or (at your option) any later version. +;;;; +;;;; This program is distributed in the hope that it will be useful, +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program; if not, write to the Free Software +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +;;;; USA. +;;;; +;;;; Copyright (C) 2018 Simon Brooke +;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;; The overall structure of this has quite closely to follow the structure of +;;; to-hugsql-queries, because essentially we need one JSON entry point to wrap +;;; each query. + +(defn file-header [application] + (list + 'ns + (symbol (str (safe-name (:name (:attrs application))) ".routes.auto-api")) + (str "API routes for " (:name (:attrs application)) + " auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at " + (f/unparse (f/formatters :basic-date-time) (t/now))) + (list + :require + '[adl-support.core :as support] + '[clj-http.client :as client] + '[clojure.tools.logging :as log] + '[compojure.api.sweet :refer :all] + '[hugsql.core :as hugsql] + '[ring.util.http-response :refer :all] + '[noir.response :as nresponse] + '[noir.util.route :as route] + '[ring.util.http-response :as response] + '[schema.core :as s] + (vector (symbol (str (safe-name (:name (:attrs application))) ".db.core")) :as 'db)))) + +