Merge branch 'feature/15' into develop

This commit is contained in:
Simon Brooke 2018-09-17 15:14:52 +01:00
commit 7ce041ad0c
10 changed files with 172 additions and 59 deletions

View file

@ -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. 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 ## 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. 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]() 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 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 description, new database schemas are generated. The database initialisation script will
be found at `resources/sql/youyesyet.postgres.sql`. Reference data initialisation scripts be found at `resources/sql/youyesyet.postgres.sql`. Manually maintained overrides are found in
will in due course be stored in the same directory. `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 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 [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 To download and install Javascript delendencies, run
cd youyesyet
lein npm install lein npm install
To start a development web server for the application, run: 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 lein figwheel
which will aid in work on the ClojureScript components.
## Running in a production environment ## 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. 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.

View file

@ -53,6 +53,7 @@ FROM followuprequests as request,
where not exists (select * from followupactions as action where not exists (select * from followupactions as action
where action.request_id = request.id where action.request_id = request.id
and action.closed = true) and action.closed = true)
and request.locked_by is null
and request.elector_id = electors.id and request.elector_id = electors.id
and request.visit_id = visits.id and request.visit_id = visits.id
and visits.address_id = addresses.id and visits.address_id = addresses.id

View file

@ -30,6 +30,7 @@ SELECT electors.name ||', '|| addresses.address ||', '|| addresses.postcode ||',
followupactions.request_id, followupactions.request_id,
canvassers.username ||', '|| canvassers.fullname ||', '|| addresses.address ||', '|| addresses.postcode ||', '|| canvassers.phone ||', '|| canvassers.email AS actor_expanded, canvassers.username ||', '|| canvassers.fullname ||', '|| addresses.address ||', '|| addresses.postcode ||', '|| canvassers.phone ||', '|| canvassers.email AS actor_expanded,
followupactions.actor, followupactions.actor,
canvassers.fullname AS actor_name,
followupactions.date, followupactions.date,
followupactions.notes, followupactions.notes,
followupactions.closed, followupactions.closed,
@ -43,3 +44,10 @@ WHERE followupactions.request_id = followuprequests.id
AND followupactions.actor = canvassers.id AND followupactions.actor = canvassers.id
; ;
GRANT SELECT ON lv_followupactions TO canvassers, issueexperts; 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;

View file

@ -1,5 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block extra-head %}
<meta http-equiv="refresh" content="60">
{% endblock %}
{% block back-links %} {% block back-links %}
<div> <div>
<div class='back-link-container'> <div class='back-link-container'>
@ -81,9 +85,13 @@ Method
{{ record.raised }} {{ record.raised }}
</td> </td>
<td> <td>
<a href='{{servlet-context}}/form-issues-Issue?id={{ record.issue_id }}'> {% ifequal record.issue_id "Other" %}
{{ record.issue_id_expanded }} {{ record.issue_detail }}
</a> {% else %}
<a href='{{servlet-context}}/form-issues-Issue?id={{ record.issue_id }}'>
{{ record.issue_id }}
</a>
{% endifequal %}
</td> </td>
<td> <td>
<a href='{{servlet-context}}/form-followupmethods-Followupmethod?id={{ record.method_id }}'> <a href='{{servlet-context}}/form-followupmethods-Followupmethod?id={{ record.method_id }}'>

View file

@ -49,12 +49,12 @@
Issue Issue
</label> </label>
{% ifmemberof issueexperts analysts issueeditors admin %} {% ifmemberof issueexperts analysts issueeditors admin %}
<span id='visit' name='visit' class='pseudo-widget disabled'> <span id='issue_id' name='issue_id' class='pseudo-widget disabled'>
{{issue.id}} {{issue.id}} <em>{{record.issue_detail}}</em>
</span> </span>
{% else %} {% else %}
<span id='visit_id' name='visit_id' class='pseudo-widget not-authorised'> <span id='issue_id' name='issue_id' class='pseudo-widget not-authorised'>
You are not permitted to view visit of followuprequests You are not permitted to view issue of followuprequests
</span> </span>
{% endifmemberof %} {% endifmemberof %}
{% ifmemberof issueexperts admin %} {% ifmemberof issueexperts admin %}
@ -104,7 +104,7 @@
<tbody> <tbody>
{% for action in actions %} {% for action in actions %}
<tr> <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.date}}</td>
<td>{{action.closed}}</td> <td>{{action.closed}}</td>
</tr> </tr>
@ -116,13 +116,13 @@
</table> </table>
</p> </p>
{% endif %} {% endif %}
{% if not closed %}
<p class='widget'> <p class='widget'>
<label for='notes'> <label for='notes'>
Your notes Your notes
</label> </label>
{% ifmemberof admin issueexperts %} {% ifmemberof admin issueexperts %}
<textarea rows='8' cols='60' id='notes' name='notes'> <textarea rows='8' cols='60' id='notes' name='notes'></textarea>
</textarea>
{% endifmemberof %} {% endifmemberof %}
</p> </p>
<p class='widget'> <p class='widget'>
@ -156,6 +156,7 @@
<input id='save-button' name='save-button' class='action-safe' type='submit' value='Save!'/> <input id='save-button' name='save-button' class='action-safe' type='submit' value='Save!'/>
</p> </p>
{% endifmemberof %} {% endifmemberof %}
{% endif %}
</form> </form>
</div> </div>

View file

@ -3,6 +3,7 @@
(:require [adl-support.core :as support] (:require [adl-support.core :as support]
[adl-support.utils :refer [safe-name]] [adl-support.utils :refer [safe-name]]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.java.jdbc :as jdbc]
[clojure.string :as s] [clojure.string :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.walk :refer [keywordize-keys]] [clojure.walk :refer [keywordize-keys]]
@ -11,6 +12,7 @@
[noir.util.route :as route] [noir.util.route :as route]
[ring.util.http-response :as response] [ring.util.http-response :as response]
[youyesyet.config :refer [env]] [youyesyet.config :refer [env]]
[youyesyet.db.core :refer [*db*]]
[youyesyet.db.core :as db] [youyesyet.db.core :as db]
[youyesyet.layout :as layout] [youyesyet.layout :as layout]
[youyesyet.oauth :as oauth] [youyesyet.oauth :as oauth]
@ -51,72 +53,122 @@
(let [user (:user (:session request))] (let [user (:user (:session request))]
{:title "Open requests" {:title "Open requests"
:user user :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] (defn get-followup-request-page [request]
(let (let
[params (support/massage-params request) [user (:user (:session request))
params (support/massage-params request)
id (:id params) id (:id params)
record (db/get-followuprequest db/*db* {:id id}) record (get-and-lock-followuprequest! id user)
elector (if elector (if
record record
(first (first
(db/search-strings-electors (db/search-strings-electors
db/*db* {:id (:elector_id record)}))) *db* {:id (:elector_id record)})))
visit (if visit (if
record record
(first (first
(db/search-strings-visits (db/search-strings-visits
db/*db* {:id (:visit_id record)})))] *db* {:id (:visit_id record)})))
actions (db/list-followupactions-by-followuprequest
*db* {:id id})]
(if record
(layout/render (layout/render
"issue-expert/request.html" "issue-expert/request.html"
{:actions (map {:actions (map
;; HTML-ise the notes in each action record ;; HTML-ise the notes in each action record
#(merge % {:notes (md-to-html-string (:notes %))}) #(merge % {:notes (md-to-html-string (:notes %))})
(db/list-followupactions-by-followuprequest actions)
db/*db* {:id id}))
:elector elector :elector elector
:issue (let :issue (let
[raw-issue (if [raw-issue (if
record record
(db/get-issue db/*db* {:id (:issue_id record)}))] (db/get-issue *db* {:id (:issue_id record)}))]
(if raw-issue (if raw-issue
(merge (merge
raw-issue raw-issue
{:brief (md-to-html-string (:brief raw-issue))}))) {:brief (md-to-html-string (:brief raw-issue))})))
:options (db/list-options db/*db* params) :options (db/list-options *db* params)
:record record :record record
:title (str "Request from " (:name elector) " at " (:date visit)) :title (str "Request from " (:name elector) " at " (:date visit))
:user (:user (:session request)) :user (:user (:session request))
:visit visit}))) :visit visit
:closed (some :closed actions)})
(list-page (assoc request :error "That request is locked")))))
(defn post-followup-action (defn post-followup-action
"From this `request`, create a `followupaction` record, and, if an "From this `request`, create a `followupaction` record, and, if an
`option_id` is present in the params, an `intention` record; show `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] [request]
(support/do-or-log-error (support/do-or-log-error
(let (let
[params (support/massage-params request) [user (:user (:session request))
locality (:locality (db/get-locality-for-visit db/*db* {:id (:visit_id params)}))] 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) (log/debug "post-followup-request-page with request " request)
(support/do-or-log-error
(jdbc/with-db-transaction [*db* *db*]
(db/create-followupaction! (db/create-followupaction!
db/*db* *db*
(assoc (assoc
params params
:actor (:id (:user (:session request))) :actor (:id user)
:date (jt/to-sql-timestamp (jt/local-date-time)) :date (jt/to-sql-timestamp (jt/local-date-time))
:closed (= (:closed params) "on"))) :closed (= (:closed params) "on")))
(release-followuprequest! (:id params) user)))
(if-not (if-not
(zero? (count (:option_id params))) (zero? (count (:option_id params)))
(if (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 ;; 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 ;; 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)) (list-page request))
:error-return :error-return
(get-followup-request-page request))) (get-followup-request-page request)))

View file

@ -157,7 +157,7 @@
without having recorded the visit, so let's not muck about." without having recorded the visit, so let's not muck about."
[request] [request]
(let [params (merge (let [params (merge
{:actions nil, :issue_detail ""} {:actions nil, :issue_detail nil :locked_by nil :locked nil}
(assoc (assoc
(massage-params request) (massage-params request)
:visit_id (current-visit-id request)))] :visit_id (current-visit-id request)))]

View file

@ -344,7 +344,8 @@
{:address_id (-> db :address :id) {:address_id (-> db :address :id)
:elector_id (-> db :elector :id) :elector_id (-> db :elector :id)
:issue_id (name (-> db :issue)) :issue_id (name (-> db :issue))
:method_id "Phone" :issue_detail (-> db :issue-detail)
:method_id (-> db :followupmethod)
:method_detail (-> db :method_detail) :method_detail (-> db :method_detail)
:action :create-request}) :action :create-request})
:send-request)) :send-request))
@ -468,6 +469,12 @@
(js/console.log (str "Setting issue to " issue)) (js/console.log (str "Setting issue to " issue))
(assoc (clear-messages db) :issue (keyword 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 (reg-event-db
:set-latitude :set-latitude

View file

@ -44,6 +44,9 @@
dwelling @(subscribe [:dwelling]) dwelling @(subscribe [:dwelling])
method @(subscribe [:followupmethod])] method @(subscribe [:followupmethod])]
(js/console.log (str "followup/panel; Issue is " issue "; elector is " elector "; method is " method " (" (type method) ")")) (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 (cond
(nil? dwelling) (nil? dwelling)
(ui/error-panel "No dwelling selected") (ui/error-panel "No dwelling selected")
@ -72,7 +75,8 @@
(if (= issue :Other) (if (= issue :Other)
[:p.widget [:p.widget
[:label {:for "issue_detail"} "Issue detail"] [: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 [:p.widget
[:label {:for "method"} "Method"] [:label {:for "method"} "Method"]
[:select {:id "method" :name "method" :defaultValue "Phone" [:select {:id "method" :name "method" :defaultValue "Phone"

View file

@ -703,11 +703,16 @@ version="0.1.1">
column="method_id" entity="followupmethods" farkey="id"> column="method_id" entity="followupmethods" farkey="id">
<prompt prompt="method_id" locale="en_GB.UTF-8"/> <prompt prompt="method_id" locale="en_GB.UTF-8"/>
</property> </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> <documentation>
Phone number or email address for followup. Phone number or email address for followup.
</documentation> </documentation>
</property> </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"/> <property type="list" name="actions" entity="followupactions" farkey="request_id"/>
<list properties="listed" name="Followuprequests"> <list properties="listed" name="Followuprequests">
<field property="elector_id"> <field property="elector_id">