Merge branch 'feature/15' into develop
This commit is contained in:
		
						commit
						7ce041ad0c
					
				
							
								
								
									
										39
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block extra-head %} | ||||
| <meta http-equiv="refresh" content="60"> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block back-links %} | ||||
| <div> | ||||
| <div class='back-link-container'> | ||||
|  | @ -81,9 +85,13 @@ Method | |||
| {{ record.raised }} | ||||
| </td> | ||||
| <td> | ||||
| <a href='{{servlet-context}}/form-issues-Issue?id={{ record.issue_id }}'> | ||||
| {{ record.issue_id_expanded }} | ||||
| </a> | ||||
|   {% ifequal record.issue_id "Other" %} | ||||
|   {{ record.issue_detail }} | ||||
|   {% else %} | ||||
|   <a href='{{servlet-context}}/form-issues-Issue?id={{ record.issue_id }}'> | ||||
|     {{ record.issue_id }} | ||||
|   </a> | ||||
|   {% endifequal %} | ||||
| </td> | ||||
| <td> | ||||
| <a href='{{servlet-context}}/form-followupmethods-Followupmethod?id={{ record.method_id }}'> | ||||
|  |  | |||
|  | @ -49,12 +49,12 @@ | |||
|         Issue | ||||
|       </label> | ||||
|       {% ifmemberof issueexperts analysts issueeditors admin %} | ||||
|       <span id='visit' name='visit' class='pseudo-widget disabled'> | ||||
|         {{issue.id}} | ||||
|       <span id='issue_id' name='issue_id' class='pseudo-widget disabled'> | ||||
|         {{issue.id}} <em>{{record.issue_detail}}</em> | ||||
|       </span> | ||||
|       {% else %} | ||||
|       <span id='visit_id' name='visit_id' class='pseudo-widget not-authorised'> | ||||
|         You are not permitted to view visit of followuprequests | ||||
|       <span id='issue_id' name='issue_id' class='pseudo-widget not-authorised'> | ||||
|         You are not permitted to view issue of followuprequests | ||||
|       </span> | ||||
|       {% endifmemberof %} | ||||
|       {% ifmemberof issueexperts admin %} | ||||
|  | @ -104,7 +104,7 @@ | |||
|         <tbody> | ||||
|           {% for action in actions %} | ||||
|           <tr> | ||||
|             <td>{{action.actor}}</td> | ||||
|             <td><a href="{{servlet-context}}/form-canvassers-Canvasser?id={{action.actor}}">{{action.actor_name}}</a></td> | ||||
|             <td>{{action.date}}</td> | ||||
|             <td>{{action.closed}}</td> | ||||
|           </tr> | ||||
|  | @ -116,13 +116,13 @@ | |||
|       </table> | ||||
|     </p> | ||||
|     {% endif %} | ||||
|     {% if not closed %} | ||||
|     <p class='widget'> | ||||
|       <label for='notes'> | ||||
|         Your notes | ||||
|       </label> | ||||
|       {% ifmemberof admin issueexperts %} | ||||
|       <textarea rows='8' cols='60' id='notes' name='notes'> | ||||
|       </textarea> | ||||
|       <textarea rows='8' cols='60' id='notes' name='notes'></textarea> | ||||
|       {% endifmemberof %} | ||||
|     </p> | ||||
|     <p class='widget'> | ||||
|  | @ -156,6 +156,7 @@ | |||
|     <input id='save-button' name='save-button' class='action-safe' type='submit' value='Save!'/> | ||||
|     </p> | ||||
|     {% endifmemberof %} | ||||
|     {% endif %} | ||||
|   </form> | ||||
| </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -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))) | ||||
|  |  | |||
|  | @ -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)))] | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -703,11 +703,16 @@ version="0.1.1"> | |||
|     column="method_id" entity="followupmethods" farkey="id"> | ||||
|       <prompt prompt="method_id" locale="en_GB.UTF-8"/> | ||||
|     </property> | ||||
|     <property required="true" type="string" name="method-detail" size="128"> | ||||
|     <property required="true" type="string" name="method_detail" column="method_detail" size="128"> | ||||
|       <documentation> | ||||
|         Phone number or email address for followup. | ||||
|       </documentation> | ||||
|     </property> | ||||
|     <property type='entity' entity='canvassers' name='locked_by' column='locked_by'> | ||||
|       <documentation>The issue expert who is currently handling this issue, if any.</documentation> | ||||
|       <prompt prompt="Locked by" locale="en_GB.UTF-8"/> | ||||
|     </property> | ||||
|     <property type="timestamp" name="locked"/> | ||||
|     <property type="list" name="actions" entity="followupactions" farkey="request_id"/> | ||||
|     <list properties="listed" name="Followuprequests"> | ||||
|       <field property="elector_id"> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue