diff --git a/README.md b/README.md index f21afe9..74f4f00 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,20 @@ If you're thinking of joining in development on this I'd strongly recommend you You should also read the [User-Oriented Specification](doc/specification/userspec.md) and any other documentation which appears under the *doc/specification* hierarchy. +## Building this + +This application is built using [Application Description Language](); the intention is that soon Application Description Language will run as a Leiningen plugin, but that does not yet work. + +So first you must check out the Application Description Language repository as well as this repository, ideally within a a common directory; + +then: + + cd adl + lein uberjar + java -jar target/adl-1.4.4-SNAPSHOT-standalone.jar --path ../youyesyet/ ../youyesyet/youyesyet.adl.xml + +This will generate a large number of the source files required by YouYesYet, **including** the database initialisation scripts. + ## Getting the database up You'll need a file *profiles.clj*, with content similar to the following; it's not in the repository because it contains passwords. @@ -62,8 +76,16 @@ Do get the database initialised, run I'm no longer using Migratus as I'm using [Application Description Language]() to generate the majority of the application, and, as changes are made to the application description, new database schemas are generated. The database initialisation script will -be found at `resources/sql/youyesyet.postgres.sql`. Reference data initialisation scripts -will in due course be stored in the same directory. +be found at `resources/sql/youyesyet.postgres.sql`. Manually maintained overrides are found in +`resources/sql/youyesyet.postgres.overrides.sql`. So to initialise the database, invoke + + psql youyesyet_dev < resources/sql/youyesyet.postgres.sql + +followed by + + psql youyesyet_dev < resources/sql/youyesyet.postgres.overrides.sql + +Reference data initialisation scripts will in due course be stored in the same directory. Once we have a more or less finished application it may be worth going back to [Migratus](https://github.com/yogthos/migratus); I might have a go at generating migrations from @@ -75,20 +97,25 @@ To run in a dev environment, checkout the *develop* branch To download and install Javascript delendencies, run + cd youyesyet lein npm install To start a development web server for the application, run: - lein run +Then -If you're wanting to work on cljs development, you need two terminal sessions. In one run + lein repl - lein run +Wait for the clojure `user=>` prompt to appear, and enter -as above; in the other, run + (mount/start) + +This will get the application running for development; ideally, open a new terminal and invoke lein figwheel +which will aid in work on the ClojureScript components. + ## Running in a production environment Doesn't really work yet; if you want to try it, see [Bug #36](https://github.com/simon-brooke/youyesyet/issues/36) and check out the associated feature branch. diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 0705af8..855342f 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -53,6 +53,7 @@ FROM followuprequests as request, where not exists (select * from followupactions as action where action.request_id = request.id and action.closed = true) +and request.locked_by is null and request.elector_id = electors.id and request.visit_id = visits.id and visits.address_id = addresses.id diff --git a/resources/sql/youyesyet.postgres.overrides.sql b/resources/sql/youyesyet.postgres.overrides.sql index d0c7a9b..f716dbf 100644 --- a/resources/sql/youyesyet.postgres.overrides.sql +++ b/resources/sql/youyesyet.postgres.overrides.sql @@ -30,6 +30,7 @@ SELECT electors.name ||', '|| addresses.address ||', '|| addresses.postcode ||', followupactions.request_id, canvassers.username ||', '|| canvassers.fullname ||', '|| addresses.address ||', '|| addresses.postcode ||', '|| canvassers.phone ||', '|| canvassers.email AS actor_expanded, followupactions.actor, + canvassers.fullname AS actor_name, followupactions.date, followupactions.notes, followupactions.closed, @@ -43,3 +44,10 @@ WHERE followupactions.request_id = followuprequests.id AND followupactions.actor = canvassers.id ; GRANT SELECT ON lv_followupactions TO canvassers, issueexperts; + + +------------------------------------------------------------------------ +-- request locking +------------------------------------------------------------------------ +ALTER TABLE followuprequests ADD COLUMN locked_by INTEGER REFERENCES canvassers(id) ON DELETE SET NULL; +ALTER TABLE followuprequests ADD COLUMN locked TIMESTAMP; diff --git a/resources/templates/issue-expert/list.html b/resources/templates/issue-expert/list.html index 8e1921b..5d929da 100644 --- a/resources/templates/issue-expert/list.html +++ b/resources/templates/issue-expert/list.html @@ -1,5 +1,9 @@ {% extends "base.html" %} +{% block extra-head %} + +{% endblock %} + {% block back-links %}
diff --git a/src/clj/youyesyet/routes/issue_experts.clj b/src/clj/youyesyet/routes/issue_experts.clj index 3f200a9..02b6e7f 100644 --- a/src/clj/youyesyet/routes/issue_experts.clj +++ b/src/clj/youyesyet/routes/issue_experts.clj @@ -3,6 +3,7 @@ (:require [adl-support.core :as support] [adl-support.utils :refer [safe-name]] [clojure.java.io :as io] + [clojure.java.jdbc :as jdbc] [clojure.string :as s] [clojure.tools.logging :as log] [clojure.walk :refer [keywordize-keys]] @@ -11,6 +12,7 @@ [noir.util.route :as route] [ring.util.http-response :as response] [youyesyet.config :refer [env]] + [youyesyet.db.core :refer [*db*]] [youyesyet.db.core :as db] [youyesyet.layout :as layout] [youyesyet.oauth :as oauth] @@ -51,72 +53,122 @@ (let [user (:user (:session request))] {:title "Open requests" :user user - :records (db/list-open-requests db/*db* {:expert (:id user)})}))) + :records (db/list-open-requests *db* {:expert (:id user)})}))) + + +(defn get-and-lock-followuprequest! + "Return the `followuprequest` record indicated by this `id`, provided that + it is unlocked. As a side effect, lock it to this `user`." + [id user] + (support/do-or-log-error + (jdbc/with-db-transaction [*db* *db*] + (let [record (db/get-followuprequest *db* {:id id})] + (if-not + (:locked record) + (do + (db/update-followuprequest! + *db* + (assoc + record + :locked_by (:id user) + :locked (jt/to-sql-timestamp (jt/local-date-time)))) + record)))) + :error-return nil)) + + +(defn release-followuprequest! + "Release the lock held on the `followuprequest` record indicated by this + `id` held by this `user`, if present." + [id user] + (log/debug "release-followuprequest! Attempting to unlock followuprequest " id) + (support/do-or-log-error + (jdbc/with-db-transaction [*db* *db*] + (let [record (db/get-followuprequest *db* {:id id})] + (if + (= (:locked_by record) (:id user)) + (do + (db/update-followuprequest! + *db* + (assoc + record + :locked_by nil + :locked nil)) + true)))) + :error-return nil)) (defn get-followup-request-page [request] (let - [params (support/massage-params request) + [user (:user (:session request)) + params (support/massage-params request) id (:id params) - record (db/get-followuprequest db/*db* {:id id}) + record (get-and-lock-followuprequest! id user) elector (if record (first (db/search-strings-electors - db/*db* {:id (:elector_id record)}))) + *db* {:id (:elector_id record)}))) visit (if record (first (db/search-strings-visits - db/*db* {:id (:visit_id record)})))] - (layout/render - "issue-expert/request.html" - {:actions (map - ;; HTML-ise the notes in each action record - #(merge % {:notes (md-to-html-string (:notes %))}) - (db/list-followupactions-by-followuprequest - db/*db* {:id id})) - :elector elector - :issue (let - [raw-issue (if - record - (db/get-issue db/*db* {:id (:issue_id record)}))] - (if raw-issue - (merge - raw-issue - {:brief (md-to-html-string (:brief raw-issue))}))) - :options (db/list-options db/*db* params) - :record record - :title (str "Request from " (:name elector) " at " (:date visit)) - :user (:user (:session request)) - :visit visit}))) + *db* {:id (:visit_id record)}))) + actions (db/list-followupactions-by-followuprequest + *db* {:id id})] + (if record + (layout/render + "issue-expert/request.html" + {:actions (map + ;; HTML-ise the notes in each action record + #(merge % {:notes (md-to-html-string (:notes %))}) + actions) + :elector elector + :issue (let + [raw-issue (if + record + (db/get-issue *db* {:id (:issue_id record)}))] + (if raw-issue + (merge + raw-issue + {:brief (md-to-html-string (:brief raw-issue))}))) + :options (db/list-options *db* params) + :record record + :title (str "Request from " (:name elector) " at " (:date visit)) + :user (:user (:session request)) + :visit visit + :closed (some :closed actions)}) + (list-page (assoc request :error "That request is locked"))))) (defn post-followup-action "From this `request`, create a `followupaction` record, and, if an `option_id` is present in the params, an `intention` record; show - the request list on success, to the request form on failure." + the request list on success, the request form on failure." [request] (support/do-or-log-error (let - [params (support/massage-params request) - locality (:locality (db/get-locality-for-visit db/*db* {:id (:visit_id params)}))] + [user (:user (:session request)) + params (support/massage-params request) + locality (:locality (db/get-locality-for-visit *db* {:id (:visit_id params)}))] (log/debug "post-followup-request-page with request " request) - (db/create-followupaction! - db/*db* - (assoc - params - :actor (:id (:user (:session request))) - :date (jt/to-sql-timestamp (jt/local-date-time)) - :closed (= (:closed params) "on"))) + (support/do-or-log-error + (jdbc/with-db-transaction [*db* *db*] + (db/create-followupaction! + *db* + (assoc + params + :actor (:id user) + :date (jt/to-sql-timestamp (jt/local-date-time)) + :closed (= (:closed params) "on"))) + (release-followuprequest! (:id params) user))) (if-not (zero? (count (:option_id params))) (if - (zero? (count (:signature (db/get-elector db/*db* {:id (:elector_id params)})))) + (zero? (count (:signature (db/get-elector *db* {:id (:elector_id params)})))) ;; the elector has NOT recorded GDPR consent: explicitly bind elector_id to nil - (db/create-intention! db/*db* (assoc params :locality locality :elector_id nil)) + (db/create-intention! *db* (assoc params :locality locality :elector_id nil)) ;; else the elector HAS recorded GDPR consent - (db/create-intention! db/*db* (assoc params :locality locality)))) + (db/create-intention! *db* (assoc params :locality locality)))) (list-page request)) :error-return (get-followup-request-page request))) diff --git a/src/clj/youyesyet/routes/rest.clj b/src/clj/youyesyet/routes/rest.clj index fad5b60..cf6d85c 100644 --- a/src/clj/youyesyet/routes/rest.clj +++ b/src/clj/youyesyet/routes/rest.clj @@ -157,7 +157,7 @@ without having recorded the visit, so let's not muck about." [request] (let [params (merge - {:actions nil, :issue_detail ""} + {:actions nil, :issue_detail nil :locked_by nil :locked nil} (assoc (massage-params request) :visit_id (current-visit-id request)))] diff --git a/src/cljs/youyesyet/canvasser_app/handlers.cljs b/src/cljs/youyesyet/canvasser_app/handlers.cljs index 73d6798..0c2ec97 100644 --- a/src/cljs/youyesyet/canvasser_app/handlers.cljs +++ b/src/cljs/youyesyet/canvasser_app/handlers.cljs @@ -344,7 +344,8 @@ {:address_id (-> db :address :id) :elector_id (-> db :elector :id) :issue_id (name (-> db :issue)) - :method_id "Phone" + :issue_detail (-> db :issue-detail) + :method_id (-> db :followupmethod) :method_detail (-> db :method_detail) :action :create-request}) :send-request)) @@ -468,6 +469,12 @@ (js/console.log (str "Setting issue to " issue)) (assoc (clear-messages db) :issue (keyword issue)))) +(reg-event-db + :set-issue-detail + (fn [db [_ issue-detail]] + (js/console.log (str "Setting issue-detail to " issue-detail)) + (assoc (clear-messages db) :issue-detail issue-detail))) + (reg-event-db :set-latitude diff --git a/src/cljs/youyesyet/canvasser_app/views/followup.cljs b/src/cljs/youyesyet/canvasser_app/views/followup.cljs index dbc85c9..ae6644e 100644 --- a/src/cljs/youyesyet/canvasser_app/views/followup.cljs +++ b/src/cljs/youyesyet/canvasser_app/views/followup.cljs @@ -44,6 +44,9 @@ dwelling @(subscribe [:dwelling]) method @(subscribe [:followupmethod])] (js/console.log (str "followup/panel; Issue is " issue "; elector is " elector "; method is " method " (" (type method) ")")) + (dispatch [:set-followupmethod "Phone"]) + (dispatch [:set-method-detail nil]) + (dispatch [:set-issue-detail nil]) (cond (nil? dwelling) (ui/error-panel "No dwelling selected") @@ -72,7 +75,8 @@ (if (= issue :Other) [:p.widget [:label {:for "issue_detail"} "Issue detail"] - [:input {:type "text" :id "issue_detail" :name "issue_detail"}]]) + [:input {:type "text" :id "issue_detail" :name "issue_detail" + :on-change #(dispatch [:set-issue-detail (.-value (.-target %))])}]]) [:p.widget [:label {:for "method"} "Method"] [:select {:id "method" :name "method" :defaultValue "Phone" diff --git a/youyesyet.adl.xml b/youyesyet.adl.xml index 6bd7c8d..7c9b44f 100644 --- a/youyesyet.adl.xml +++ b/youyesyet.adl.xml @@ -703,11 +703,16 @@ version="0.1.1"> column="method_id" entity="followupmethods" farkey="id"> - + Phone number or email address for followup. + + The issue expert who is currently handling this issue, if any. + + +