No
diff --git a/resources/templates/untitled.html b/resources/templates/untitled.html
new file mode 100644
index 0000000..e69de29
diff --git a/src/clj/youyesyet/authorisation.clj b/src/clj/youyesyet/authorisation.clj
new file mode 100644
index 0000000..2ca529b
--- /dev/null
+++ b/src/clj/youyesyet/authorisation.clj
@@ -0,0 +1,4 @@
+(ns ^{:doc "Field-level authorisation. Messy."
+ :author "Simon Brooke"}
+ youyesyet.authorisation
+ (:require [youyesyet.env :refer [defaults]]))
diff --git a/src/clj/youyesyet/db/core.clj b/src/clj/youyesyet/db/core.clj
index a57fa50..0cd74b0 100644
--- a/src/clj/youyesyet/db/core.clj
+++ b/src/clj/youyesyet/db/core.clj
@@ -1,28 +1,30 @@
-(ns ^{:doc "Database access functions."
- :author "Simon Brooke"}
+(ns ^{:doc "Database access functions, mostly from Luminus template."}
youyesyet.db.core
(:require
[cheshire.core :refer [generate-string parse-string]]
[clojure.java.jdbc :as jdbc]
[conman.core :as conman]
- [youyesyet.config :refer [env]]
- [mount.core :refer [defstate]])
+ [hugsql.core :as hugsql]
+ [mount.core :refer [defstate]]
+ [youyesyet.config :refer [env]])
(:import org.postgresql.util.PGobject
java.sql.Array
clojure.lang.IPersistentMap
clojure.lang.IPersistentVector
[java.sql
- BatchUpdateException
+ ;; BatchUpdateException
Date
Timestamp
PreparedStatement]))
(defstate ^:dynamic *db*
- :start (conman/connect! {:jdbc-url (env :database-url)
- :driver-class-name "org.postgresql.Driver"})
- :stop (conman/disconnect! *db*))
+ :start (conman/connect! {:jdbc-url-env (env :database-url)
+ :jdbc-url "jdbc:postgresql://127.0.0.1/youyesyet_dev?user=youyesyet&password=thisisnotsecure"
+ :driver-class-name "org.postgresql.Driver"})
+ :stop (conman/disconnect! *db*))
-(conman/bind-connection *db* "sql/queries.sql")
+(conman/bind-connection *db* "sql/queries.auto.sql" "sql/queries.sql")
+(hugsql/def-sqlvec-fns "sql/queries.auto.sql")
(defn to-date [^java.sql.Date sql-date]
(-> sql-date (.getTime) (java.util.Date.)))
diff --git a/src/clj/youyesyet/db/schema.clj b/src/clj/youyesyet/db/schema.clj
deleted file mode 100644
index 7571251..0000000
--- a/src/clj/youyesyet/db/schema.clj
+++ /dev/null
@@ -1,484 +0,0 @@
-(ns ^{:doc "Korma-flavour database setup, now obsolete but retained for documentation."
- :author "Simon Brooke"} youyesyet.db.schema
- (:require [clojure.java.jdbc :as sql]
- [korma.core :as kc]
- [youyesyet.db.core :as yyydb]))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;
-;;;; youyesyet.db.schema: database schema for youyesyet.
-;;;;
-;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
-;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-
-;;; Note that this is the (old) Korma way of doing things;
-;;; it may not play well with migrations, nor with the HugSQL way of doing things recommended
-;;; in Web Development with Clojure, Second Ed. So this may be temporary 'get us started' code,
-;;; which later gets thrown away. The 'create-x-table!' functions in this file may be
-;;; redundant, and if they are the namespace probably needs to be renamed to 'entities'.
-;;; See also resources/migrations/20161014170335-basic-setup.up.sql
-
-(defn create-districts-table!
- "Create a table to hold the electoral districts in which electors are registered.
- Note that, as this app is being developed for the independence referendum in which
- polling is across the whole of Scotland, this part of the design isn't fully thought
- through; if later adapted to general or local elections, some breakdown or hierarchy
- of polling districts into constituencies will be required."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :districts
- ;; it may be necessary to have a serial abstract primary key but I suspect
- ;; polling districts already have numbers assigned by the Electoral Commission and
- ;; it would be sensible to use those. TODO: check.
- [:id "integer not null primary key"]
- [:name "varchar(64) not null"]
- ;; TODO: it would make sense to hold polygon data for polling districts so we can reflect
- ;; them on the map, but I haven't thought through how to do that yet.
- )))
-
-
-(kc/defentity district
- (kc/pk :id)
- (kc/table :districts)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :name))
-
-
-(defn create-addresses-table!
- "Create a table to hold the addresses at which electors are registered."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :addresses
- [:id "serial not null primary key"]
- ;; we do NOT want to hold multiple address records for the same household. When we receive
- ;; the electoral roll data the addresses are likely to be text fields inlined in the elector
- ;; record; in digesting the roll data we need to split these out and resolve them against existing
- ;; addresses in the table, creating a new address record only if there's no match.
- [:address "varchar(256) not null unique"]
- [:postcode "varchar(16)"]
- [:phone "varchar(16)"]
- ;; the electoral district within which this address exists
- [:district_id "integer references districts(id)"]
- [:latitude :real]
- [:longitude :real])))
-
-
-(kc/defentity address
- (kc/pk :id)
- (kc/table :addresses)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :address :postcode :phone :latitude :longitude)
- (kc/has-one district))
-
-
-(defn create-authorities-table!
- "Create a table to hold the oauth authorities against which we with authenticate canvassers."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :authorities
- [:id "varchar(32) not null primary key"]
- ;; more stuff here when I understand more
- )))
-
-
-(kc/defentity authority
- (kc/pk :id)
- (kc/table :authorities)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id))
-
-
-(defn create-electors-table!
- "Create a table to hold electors data."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :electors
- ;; id should be the roll number on the electoral roll, I think, but only if this is unique
- ;; across Scotland. Otherwise we need a separate id field. TODO: check.
- [:id "integer primary key"]
- [:name "varchar(64) not null"]
- [:address_id "integer not null references addresses(id)" ]
- [:phone "varchar(16)"]
- ;; we'll probably only capture email data on electors if they request a followup
- ;; on a particular issue by email.
- [:email "varchar(128)"])))
-
-
-(kc/defentity elector
- (kc/pk :id)
- (kc/table :electors)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :name :phone :email)
- (kc/has-one address))
-
-
-;;; Lifecycle of the canvasser record goes like this, I think:
-;;; A canvasser record is created when an existing canvasser issues an invitation to a friend.
-;;; The invitation takes the form of an automatically generated email with a magic token in it.
-;;; At this point the record has only an email address, the introduced_by and the magic token,
-;;; which is itself probably a hash of the email address. Therefore, having the username as the
-;;; primary key won't work.
-;;;
-;;; The invited person clicks on the link in the email and completes the sign-up form, adding
-;;; their full name, and their phone number. If the username they have chosen is unique, they
-;;; are then sent a second email with a new magic token, possibly a hash of email address +
-;;; full name. When they click on the link in this second email, their 'authorised' flag is
-;;; set to 'true'.
-;;;
-;;; Administrators can also create canvasser records directly.aw
-;;; TODO: Do we actually need a username at all? Wouldn't the email address do?
-
-(defn create-canvassers-table!
- "Create a table to hold data on canvassers (including authentication data)."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :canvassers
- ;; id is the username the canvasser logs in as.
- [:id "serial primary key"]
- [:username "varchar(32) unique"]
- [:fullname "varchar(64) not null"]
- ;; most canvassers will be electors, we should link them:
- [:elector_id "integer references electors(id) on delete no action"]
- ;; but some canvassers may not be electors, so we need contact details separately:
- [:address_id "integer not null references addresses(id)" ]
- [:phone "varchar(16)"]
- [:email "varchar(128)"]
- ;; with which authority do we authenticate this canvasser? I do not want to hold even
- ;; encrypted passwords locally
- [:authority_id "varchar(32) not null references authorities(id) on delete no action"]
- [:introduced_by "integer references canvassers(id)"]
- [:is_admin :boolean]
- ;; true if the canvasser is authorised to use the app; else false. This allows us to
- ;; block canvassers we suspect of misbehaving.
- [:authorised :boolean])))
-
-
-(kc/defentity canvasser
- (kc/pk :id)
- (kc/table :canvassers)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :fullname :phone :email :is_admin :authorised)
- (kc/has-one elector)
- (kc/has-one address)
-;; (kc/has-one canvasser {:fk :introduced_by})
- (kc/has-one authority))
-
-
-(defn create-visits-table!
- "Create a table to record visits by canvassers to addresses (including virtual visits by telephone)."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :visits
- [:id "serial not null primary key"]
- [:address_id "integer not null references addresses(id)"]
- [:canvasser_id "integer not null references canvassers(id)"]
- [:date "timestamp with time zone not null default now()"])))
-
-
-(kc/defentity visit
- (kc/pk :id)
- (kc/table :visits)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :date)
- (kc/has-one address)
- (kc/has-one canvasser))
-
-
-(defn create-options-table!
- "Create a table to record options in the vote. This app is being created for the Independence
- referendum, which will have just two options, 'Yes' and 'No', but it might later be adapted
- for more general political canvassing."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :options
- ;; id is also the text of the option; e.g. 'Yes', 'No'.
- [:id "varchar(32) not null primary key"]
- ;; To do elections you probably need party and candidate and stuff here, but
- ;; for the referendum it's unnecessary.
- )))
-
-
-(kc/defentity option
- (kc/pk :id)
- (kc/table :options)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id))
-
-
-(defn create-option-district-table!
- "Create a table to link options to the districts in which they are relevant. This is extremely
- simple for the referendum: both options are relevant to all districts. This table is essentially
- 'for later expansion'."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :optionsdistricts
- [:option_id "varchar(32) not null references options(option)"]
- [:district_id "integer not null references districts(id)"])))
-
-
-;; I think we don't need an entity for optionsdistricts, because it's just a link table.
-
-
-(defn create-intention-table!
- "Create a table to record the intention of an elector as solicited by a canvasser during a visit.
- TODO: decide whether to insert a record in this table for 'don't knows'."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :intentions
- [:id "serial primary key"]
- ;; the elector who gave this intention
- [:elector_id "integer not null references electors(id)"]
- ;; the option the elector said they were planning to vote for
- [:option_id "varchar(32) not null references options(option)"]
- [:visit_id "integer not null references visits(id)"])))
-
-
-(kc/defentity intention
- (kc/pk :id)
- (kc/table :intentions)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id)
- (kc/has-one elector)
- (kc/has-one option)
- (kc/has-one visit))
-
-
-(defn create-issues-table!
- "A table for issues we predict electors may raise on the doorstep, for which we may be
- able to provide extra information or arrange for issue-specialists to phone and talk
- to the elector."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :issues
- ;; short name of this issue, e.g. 'currency', 'defence', 'pensions'
- [:id "varchar(32) not null primary key"]
- ;; URL of some brief material the canvasser can use on the doorstap
- [:url "varchar(256)"])))
-
-
-(kc/defentity issue
- (kc/pk :id)
- (kc/table :issues)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :url))
-
-
-(defn create-followup-methods-table!
- "Create a table to hold reference data on followup methods."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :followupmethods
- [;; the method, e.g. 'telephone', 'email', 'post'
- :id "varchar(32) not null primary key"])))
-
-
-(kc/defentity followup-method
- (kc/pk :id)
- (kc/table :followupmethods)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id))
-
-
-(defn create-issue-expertise-table!
- "A table to record which canvassers have expertise in which issues, so that followup
- requests can be directed to the right canvassers."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :issueexpertise
- ;; the expert canvasser
- [:canvasser_id "integer not null references canvassers(id)"]
- ;; the issue they have expertise in
- [:issue_id "varchar(32) not null references issues(id)"]
- ;; the method by which this expert can respond to electors on this issue
- [:method_id "varchar(32) not null references followupmethods(id)"])))
-
-
-(kc/defentity issue-expertise
- (kc/table :issueexpertise)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id)
- (kc/has-one canvasser)
- (kc/has-one issue)
- (kc/has-one followup-method))
-
-
-(defn create-followup-requests-table!
- "Create a table to record requests for followup contacts on particular issues."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :followuprequests
- [:id "serial primary key"]
- [:elector_id "integer not null references electors(id)"]
- [:visit_id "integer not null references visits(id)"]
- [:issue_id "varchar(32) not null references issues(id)"]
- ;; We probably need a followupmethod (telephone, email, postal) and, for telephone,
- ;; convenient times but I haven't thought through how to represent this or how
- ;; the user interface will work.
- [:method_id "varchar(32) not null references followupmethods(id)"])))
-
-
-(kc/defentity followup-request
- (kc/table :followuprequests)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id)
- (kc/has-one elector)
- (kc/has-one visit)
- (kc/has-one issue)
- (kc/has-one followup-method))
-
-
-(defn create-followup-actions-table!
- "Create a table to record actions on followup requests. Record in this table are almost
- certainly created through a desktop-style interface rather than through te app, so it's
- reasonable that there should be narrative fields."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :followupactions
- [:id "serial primary key"]
- [:request_id "integer not null references followuprequests(id)"]
- [:actor "integer not null references canvassers(id)"]
- [:date "timestamp with time zone not null default now()"]
- [:notes "text"]
- ;; true if this action closes the request
- [:closed :boolean])))
-
-
-(kc/defentity followup-action
- (kc/table :followupactions)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :notes :date :closed)
- (kc/has-one followup-request)
- (kc/has-one canvasser {:fk :actor}))
-
-
-
-(defn create-role-table!
- "Create a table to record roles. I'm not even yet certain that this is strictly necessary,
- but it allows us to record the fact that different users (canvassers) have different roles
- in the system."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :roles
- [:id "serial primary key"]
- [:name "varchar(64) not null"])))
-
-
-(defn create-role-membership-table!
- "Create a link table to record membership of roles."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :rolememberships
- [:role_id "integer not null references role(id)"]
- [:canvasser_id "integer not null references canvasser(id)"])))
-
-
-(kc/defentity role
- (kc/table :roles)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :name)
- (kc/many-to-many canvasser :rolememberships))
-
-
-(defn create-team-table!
- "Create a table to record teams."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :teams
- [:id "serial primary key"]
- [:name "varchar(64) not null"]
- ;; the electoral district within which this address exists
- [:district_id "integer references districts(id)"]
- ;; nominal home location of this team
- [:latitude :real]
- [:longitude :real])))
-
-
-(defn create-team-membership-table!
- "Create a link table to record membership of team."
- []
- (sql/db-do-commands
- yyydb/*db*
- (sql/create-table-ddl
- :teammemberships
- [:team_id "integer not null references team(id)"]
- [:canvasser_id "integer not null references canvasser(id)"])))
-
-
-(kc/defentity team
- (kc/table :teams)
- (kc/database yyydb/*db*)
- (kc/entity-fields :id :name :latitude :longitude)
- (kc/has-one district)
- (kc/many-to-many canvasser :teammemberships))
-
-
-(defn init-db! []
- "Initialised the whole database."
- (create-districts-table!)
- (create-addresses-table!)
- (create-authorities-table!)
- (create-electors-table!)
- (create-canvassers-table!)
- (create-visits-table!)
- (create-options-table!)
- (create-issues-table!)
- (create-followup-methods-table!)
- (create-issue-expertise-table!)
- (create-followup-requests-table!)
- (create-followup-actions-table!)
- (create-role-table!)
- (create-role-membership-table!)
- (create-team-table!)
- (create-team-membership-table!)
- )
diff --git a/src/clj/youyesyet/handler.clj b/src/clj/youyesyet/handler.clj
index 173497e..983df79 100644
--- a/src/clj/youyesyet/handler.clj
+++ b/src/clj/youyesyet/handler.clj
@@ -1,17 +1,24 @@
(ns ^{:doc "Handlers for starting and stopping the webapp."
:author "Simon Brooke"}
youyesyet.handler
- (:require [compojure.core :refer [routes wrap-routes]]
- [youyesyet.layout :refer [error-page]]
- [youyesyet.routes.authenticated :refer [authenticated-routes]]
- [youyesyet.routes.home :refer [home-routes]]
- [youyesyet.routes.oauth :refer [oauth-routes]]
+ (:require [clojure.tools.logging :as log]
+ [compojure.core :refer [routes wrap-routes]]
[compojure.route :as route]
- [youyesyet.env :refer [defaults]]
[mount.core :as mount]
+ [noir.session :as session]
+ [youyesyet.config :refer [env]]
+ [youyesyet.layout :refer [error-page]]
[youyesyet.middleware :as middleware]
- [clojure.tools.logging :as log]
- [youyesyet.config :refer [env]]))
+ [youyesyet.routes.home :refer [home-routes]]
+ [youyesyet.routes.auto :refer [auto-selmer-routes]]
+ [youyesyet.routes.auto-json :refer [auto-rest-routes]]
+ [youyesyet.routes.issue-experts :refer [issue-expert-routes]]
+ [youyesyet.routes.logged-in :refer [logged-in-routes]]
+ [youyesyet.routes.oauth :refer [oauth-routes]]
+ [youyesyet.routes.rest :refer [rest-routes]]
+ [youyesyet.routes.roles :refer [roles-routes]]
+ [youyesyet.routes.services :refer [service-routes]]
+ [youyesyet.env :refer [defaults]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@@ -58,17 +65,40 @@
(shutdown-agents)
(log/info "youyesyet has shut down!"))
+
(def app-routes
(routes
(-> #'home-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
- #'oauth-routes
- #'authenticated-routes
+ (-> #'logged-in-routes
+ (wrap-routes middleware/wrap-csrf)
+ (wrap-routes middleware/wrap-formats))
+ (-> #'roles-routes
+ (wrap-routes middleware/wrap-csrf)
+ (wrap-routes middleware/wrap-formats))
+ (-> #'issue-expert-routes
+ (wrap-routes middleware/wrap-csrf)
+ (wrap-routes middleware/wrap-formats))
+ (-> #'auto-rest-routes
+ (wrap-routes middleware/wrap-csrf)
+ (wrap-routes middleware/wrap-formats))
+ (-> #'auto-selmer-routes
+ (wrap-routes middleware/wrap-csrf)
+ (wrap-routes middleware/wrap-formats))
+ (-> #'auto-rest-routes
+ (wrap-routes middleware/wrap-formats))
+ (-> #'rest-routes
+ (wrap-routes middleware/wrap-formats))
+ (-> #'service-routes
+ (wrap-routes middleware/wrap-formats)) ;; TODO: and authentication, but let's not sweat the small stuff.
+ 'oauth-routes
+ (route/resources "/")
(route/not-found
(:body
(error-page {:status 404
- :title "page not found"})))))
+ :title "Page not found"
+ :message "The page you requested has not yet been implemented"})))))
(def app (middleware/wrap-base #'app-routes))
diff --git a/src/clj/youyesyet/layout.clj b/src/clj/youyesyet/layout.clj
index d131cb7..a784cc6 100644
--- a/src/clj/youyesyet/layout.clj
+++ b/src/clj/youyesyet/layout.clj
@@ -1,44 +1,96 @@
-(ns^{:doc "Render web pages using Selmer tamplating markup."
- :author "Simon Brooke"}
- youyesyet.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*]]))
+(ns^{:doc "Render web pages using Selmer templating markup."
+ :author "Simon Brooke"}
+ youyesyet.layout
+ (:require [adl-support.tags :as tags]
+ [clojure.string :refer [lower-case]]
+ [clojure.tools.logging :as log]
+ [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*]]
+ [selmer.parser :as parser]
+ [selmer.filters :as filters]
+ [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db]
+ ))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.layout: lay out Selmer-templated web pages.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:dynamic *app-context*)
(parser/set-resource-path! (clojure.java.io/resource "templates"))
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
+(tags/add-tags)
+
+(defn raw-get-user-roles [user]
+ "Return, as a set, the names of the roles of which this user is a member."
+ (if
+ user
+ (do
+ (log/debug (str "seeking roles for user " user))
+ (let [roles
+ (set (map #(lower-case (:name %)) (db/list-roles-by-canvasser db/*db* user)))]
+ (log/debug (str "found roles " roles " for user " user))
+ roles))))
+
+
+;; role assignments change only rarely.
+(def get-user-roles (memoize raw-get-user-roles))
(defn render
- "renders the HTML template located relative to resources/templates"
- [template & [params]]
- (content-type
- (ok
- (parser/render-file
- template
- (assoc params
- :page template
- :csrf-token *anti-forgery-token*
- :servlet-context *app-context*
- :version (System/getProperty "youyesyet.version"))))
- "text/html; charset=utf-8"))
+ "renders the HTML `template` located relative to resources/templates in
+ the context of this session and with these parameters."
+ ;; TODO: I'm passing `session` through into render. The default luminus
+ ;; setup doesn't do this, and Dmitri is an awful lot better at this stuff
+ ;; than me so there's almost certainly a reason it doesn't.
+ [template session & [params]]
+ (let [user (:user session)]
+ (log/debug (str "layout/render: template: '" template "'"))
+ (content-type
+ (ok
+ (parser/render-file
+ template
+ (merge params
+ {:page template
+ :csrf-token *anti-forgery-token*
+ :user user
+ :user-roles (get-user-roles user)
+ :site-title (:site-title env)
+ :version (System/getProperty "youyesyet.version")})))
+ "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"
+ :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)})
+ :body (render "error.html" {} error-details)})
diff --git a/src/clj/youyesyet/middleware.clj b/src/clj/youyesyet/middleware.clj
index 2288720..fe581a2 100644
--- a/src/clj/youyesyet/middleware.clj
+++ b/src/clj/youyesyet/middleware.clj
@@ -1,17 +1,17 @@
-(ns ^{:doc "Plumbing, mainly boilerplate from Luminus."
- :author "Simon Brooke"}
+(ns ^{:doc "Plumbing, mainly boilerplate from Luminus."}
youyesyet.middleware
- (:require [youyesyet.env :refer [defaults]]
- [clojure.tools.logging :as log]
- [youyesyet.layout :refer [*app-context* error-page]]
+ (:require [clojure.tools.logging :as log]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
- [ring.middleware.webjars :refer [wrap-webjars]]
+ [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
[ring.middleware.format :refer [wrap-restful-format]]
- [youyesyet.config :refer [env]]
+ [ring.middleware.webjars :refer [wrap-webjars]]
[ring-ttl-session.core :refer [ttl-memory-store]]
- [ring.middleware.defaults :refer [site-defaults wrap-defaults]])
+ [youyesyet.env :refer [defaults]]
+ [youyesyet.config :refer [env]]
+ [youyesyet.layout :refer [*app-context* error-page]])
(:import [javax.servlet ServletContext]))
+
(defn wrap-context [handler]
(fn [request]
(binding [*app-context*
@@ -27,6 +27,7 @@
(:app-context env))]
(handler request))))
+
(defn wrap-internal-error [handler]
(fn [req]
(try
@@ -37,6 +38,7 @@
: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
@@ -45,6 +47,7 @@
{:status 403
:title "Invalid anti-forgery token"})}))
+
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
handler
@@ -54,6 +57,7 @@
;; since they're not compatible with this middleware
((if (:websocket? request) handler wrapped) request))))
+
(defn wrap-base [handler]
(-> ((:middleware defaults) handler)
wrap-webjars
diff --git a/src/clj/youyesyet/oauth.clj b/src/clj/youyesyet/oauth.clj
index 89b8068..e96c29b 100644
--- a/src/clj/youyesyet/oauth.clj
+++ b/src/clj/youyesyet/oauth.clj
@@ -1,35 +1,94 @@
-(ns youyesyet.oauth
+(ns ^{:doc "Handle oauth with multiple authenticating authorities."
+ :author "Simon Brooke"} youyesyet.oauth
(:require [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db]
[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))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.routes.home: routes and pages for unauthenticated users.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(defn get-authorities
+ "Fetch the authorities from the database and return a map of them."
+ [_]
+ (reduce
+ merge
+ {}
+ (map
+ (fn [authority]
+ (hash-map
+ (:id authority)
+ (oauth/make-consumer
+ (:consumer_key authority)
+ (:consumer_secret authority)
+ (:request_token_uri authority)
+ (:access_token_uri authority)
+ (:authorize_uri authority)
+ :hmac-sha1)))
+ (db/list-authorities db/*db* {}))))
+
+(def authority!
+ ;; Closure to allow authorities map to be created once when the function is first
+ ;; called. The argument `id` should be a string, the id of some authority
+ ;; known to the database. As side-effect, the authorities map is bound in the
+ ;; closure.
+ (let [authorities (atom nil)]
+ (fn [id]
+ (if
+ (nil? @authorities)
+ (do
+ (log/debug "Initialising authorities map")
+ (swap!
+ authorities
+ get-authorities)))
+ (let
+ [authority (@authorities id)]
+ (if authority
+ (do
+ (log/debug (str "Selected authority " id))
+ authority))))))
(defn oauth-callback-uri
- "Generates the oauth request callback URI"
+ "Generates the oauth request callback URI."
[{:keys [headers]}]
- (str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/twitter-callback"))
+ (str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/oauth-callback"))
(defn fetch-request-token
- "Fetches a request token."
- [request]
+ "Fetches a request token from the authority implied by this `request`."
+ ([request auth]
(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))))
+ (oauth/request-token auth (oauth-callback-uri request))))
+ ([request]
+ (fetch-request-token request (:authority (:session request)))))
(defn fetch-access-token
- [request_token]
- (oauth/access-token consumer request_token (:oauth_verifier request_token)))
+ [request_token authority]
+ (oauth/access-token authority 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)))
+ [request-token authority]
+ (str (oauth/user-approval-uri authority request-token)))
diff --git a/src/clj/youyesyet/routes/administrator.clj b/src/clj/youyesyet/routes/administrator.clj
deleted file mode 100644
index 7a08b36..0000000
--- a/src/clj/youyesyet/routes/administrator.clj
+++ /dev/null
@@ -1,11 +0,0 @@
-(ns ^{:doc "Routes/pages available to administrators, only."
- :author "Simon Brooke"}
- youyesyet.routes.administrator
- (:require [clojure.java.io :as io]
- [clojure.walk :refer [keywordize-keys]]
- [compojure.core :refer [defroutes GET POST]]
- [noir.response :as nresponse]
- [noir.util.route :as route]
- [ring.util.http-response :as response]
- [youyesyet.layout :as layout]
- [youyesyet.db.core :as db]))
diff --git a/src/clj/youyesyet/routes/authenticated.clj b/src/clj/youyesyet/routes/authenticated.clj
deleted file mode 100644
index b02c512..0000000
--- a/src/clj/youyesyet/routes/authenticated.clj
+++ /dev/null
@@ -1,75 +0,0 @@
-(ns ^{:doc "Routes/pages available to all authenticated users."
- :author "Simon Brooke"}
- youyesyet.routes.authenticated
- (:require [clojure.java.io :as io]
- [clojure.walk :refer [keywordize-keys]]
- [compojure.core :refer [defroutes GET POST]]
- [noir.response :as nresponse]
- [noir.util.route :as route]
- [ring.util.http-response :as response]
- [youyesyet.layout :as layout]
- [youyesyet.db.core :as db]))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;
-;;;; youyesyet.routes.authenticated: routes and pages for authenticated users.
-;;;;
-;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
-;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-;;; This code adapted from http://www.luminusweb.net/docs#accessing_the_database
-
-(defn post?
- "Return true if the argument is a ring request which is a post request"
- [request]
- true)
-
-(defn canvasser-page
- "Process this canvasser request, and render the canvasser page"
- [request]
- (let [canvasser (if
- (:params request)
- (let [params (:params request)]
- (if (:id params)
- (if (post? request)
- (db/update-canvasser! params)
- (db/create-canvasser! params))
- (db/get-canvasser (:id params)))
- ))]
- (layout/render
- "canvasser.html"
- {:title (if canvasser
- (str
- "Edit canvasser "
- (:fullname canvasser)
- " "
- (:email canvasser))
- "Add new canvasser")
- :canvasser canvasser
- :address (if (:address_id canvasser) (db/get-address (:address_id canvasser)))})))
-
-(defn routing-page
- "Render the routing page, which offers routes according to the user's roles"
- []
- (layout/render "routing.html"))
-
-(defroutes authenticated-routes
- (GET "/edit-canvasser" request (canvasser-page request))
- (POST "/edit-canvasser" request (canvasser-page request))
- (GET "/routing" [] (routing-page)))
diff --git a/src/clj/youyesyet/routes/home.clj b/src/clj/youyesyet/routes/home.clj
index 9fbc094..26b3f3f 100644
--- a/src/clj/youyesyet/routes/home.clj
+++ b/src/clj/youyesyet/routes/home.clj
@@ -1,13 +1,19 @@
(ns ^{:doc "Routes/pages available to unauthenticated users."
:author "Simon Brooke"} youyesyet.routes.home
- (:require [clojure.walk :refer [keywordize-keys]]
- [noir.response :as nresponse]
+ (:require [adl-support.utils :refer [safe-name]]
+ [clojure.java.io :as io]
+ [clojure.string :as s]
+ [clojure.tools.logging :as log]
+ [clojure.walk :refer [keywordize-keys]]
+ [markdown.core :refer [md-to-html-string]]
[noir.util.route :as route]
- [youyesyet.layout :as layout]
- [youyesyet.db.core :as db-core]
- [compojure.core :refer [defroutes GET POST]]
[ring.util.http-response :as response]
- [clojure.java.io :as io]))
+ [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db-core]
+ [youyesyet.layout :as layout]
+ [youyesyet.oauth :as oauth]
+ [compojure.core :refer [defroutes GET POST]]
+ ))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@@ -32,12 +38,21 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defn app-page []
- (layout/render "app.html"))
+
+(defn motd
+ []
+ "Message of the day data is currently being loaded from a file in resources.
+ That probably isn't the final solution, but I don't currently have a final
+ solution"
+ (let [motd (io/as-file (io/resource (env :motd)))]
+ (if (.exists motd) (slurp motd) "")))
(defn about-page []
- (layout/render "about.html"))
+ (layout/render "about.html" {} {:title
+ (str "About " (:site-title env))
+
+ :motd (md-to-html-string (motd))}))
(defn call-me-page [request]
@@ -45,65 +60,76 @@
request
(do
;; do something to store it in the database
- (layout/render "call-me-accepted.html" (:params request)))
- (layout/render "call-me.html"
+ (layout/render "call-me-accepted.html" (:session request) (:params request)))
+ (layout/render "call-me.html" (:session request)
{:title "Please call me!"
;; TODO: Issues need to be fetched from the database
- :concerns nil})))
-
-
-(defn roles-page [request]
- (let
- [session (:session request)
- username (:user session)
- user (if username (db-core/get-canvasser-by-username db-core/*db* {:username username}))
- roles (if user (db-core/get-roles-by-canvasser db-core/*db* {:canvasser (:id user)}))]
- (cond
- roles (layout/render "roles.html"
- {:title (str "Welcome " (:fullname user) ", what do you want to do?")
- :user user
- :roles roles})
- (empty? roles)(response/found "/app")
- true (assoc (response/found "/login") :session (dissoc session :user))
- )))
+ :concerns (db-core/list-issues db-core/*db* {})})))
(defn home-page []
- (layout/render "home.html" {:title "You Yes Yet?"}))
+ (layout/render "home.html" {} {:title "You yes yet?"
+ :motd (md-to-html-string (motd))}))
(defn login-page
"This is very temporary. We're going to do authentication by oauth."
[request]
- (let [params (keywordize-keys (:form-params request))
+ (let [params (keywordize-keys (:params request))
session (:session request)
username (:username params)
user (if username (db-core/get-canvasser-by-username db-core/*db* {:username username}))
password (:password params)
redirect-to (or (:redirect-to params) "roles")]
(cond
- ;; this is obviously, ABSURDLY, insecure. I don't want to put just-about-good-enough,
- ;; it-will-do-for-now security in place; instead, I want this to be test code only
- ;; until we have o-auth properly working.
- (and user (= username password))
- (assoc (response/found redirect-to) :session (assoc session :user username))
- user
- (layout/render "login.html" {:title (str "User " username " is unknown") :redirect-to redirect-to})
- true
- (layout/render "login.html" {:title "Please log in" :redirect-to redirect-to}))))
+ (:authority params)
+ (let [auth (oauth/authority! (:authority params))]
+ (if auth
+ (do
+ (log/info "Attempting to authorise with authority " (:authority params))
+ (oauth/fetch-request-token
+ (assoc request :session (assoc session :authority auth))
+ auth))
+ (throw (Exception. (str "No such authority: " (:authority params))))))
+ ;; this is obviously, ABSURDLY, insecure. I don't want to put just-about-good-enough,
+ ;; it-will-do-for-now security in place; instead, I want this to be test code only
+ ;; until we have o-auth properly working.
+ (and user (= username password))
+ (let
+ [roles (layout/get-user-roles user)]
+ (log/info (str "Logged in user '" username "' with roles " roles))
+ (assoc
+ (response/found redirect-to)
+ :session
+ (assoc session :user user :roles roles)))
+ ;; if we've got a username but either no user object or else
+ ;; the password doesn't match
+ username
+ (layout/render
+ "login.html"
+ session
+ {:title (str "User " username " is unknown")
+ :redirect-to redirect-to
+ :warnings ["Your user name was not recognised or your password did not match"]})
+ ;; if we've no username, just invite the user to log in
+ true
+ (layout/render
+ "login.html"
+ session
+ {:title "Please log in"
+ :redirect-to redirect-to
+ :authorities (db-core/list-authorities db-core/*db*)}))))
(defroutes home-routes
(GET "/" [] (home-page))
(GET "/home" [] (home-page))
(GET "/about" [] (about-page))
- (GET "/roles" request (route/restricted (roles-page request)))
- (GET "/app" [] (route/restricted (app-page)))
(GET "/call-me" [] (call-me-page nil))
(POST "/call-me" request (call-me-page request))
- (GET "/auth" request (login-page request))
- (POST "/auth" request (login-page request))
- (GET "/notyet" [] (layout/render "notyet.html"
- {:title "Can we persuade you?"}))
- (GET "/supporter" [] (layout/render "supporter.html"
+ (GET "/login" request (login-page request))
+ (POST "/login" request (login-page request))
+ (GET "/notyet" [] (layout/render "notyet.html" {}
+ {:title "Can we persuade you?"}))
+ (GET "/supporter" [] (layout/render "supporter.html" {}
{:title "Have you signed up as a canvasser yet?"})))
diff --git a/src/clj/youyesyet/routes/issue_experts.clj b/src/clj/youyesyet/routes/issue_experts.clj
new file mode 100644
index 0000000..8a54566
--- /dev/null
+++ b/src/clj/youyesyet/routes/issue_experts.clj
@@ -0,0 +1,99 @@
+(ns ^{:doc "Routes/pages available to issue experts."
+ :author "Simon Brooke"} youyesyet.routes.issue-experts
+ (:require [adl-support.core :as support]
+ [adl-support.utils :refer [safe-name]]
+ [clojure.java.io :as io]
+ [clojure.string :as s]
+ [clojure.tools.logging :as log]
+ [clojure.walk :refer [keywordize-keys]]
+ [markdown.core :refer [md-to-html-string]]
+ [noir.util.route :as route]
+ [ring.util.http-response :as response]
+ [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db]
+ [youyesyet.layout :as layout]
+ [youyesyet.oauth :as oauth]
+ [compojure.core :refer [defroutes GET POST]]
+ ))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.routes.home: routes and pages for issue experts.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn list-page [request]
+ (layout/render
+ "issue-expert/list.html"
+ (:session request)
+ (let [user (:user (:session request))]
+ {:title "Open requests"
+ :user user
+ :records (db/list-open-requests db/*db* {:expert (:id user)})})))
+
+
+(defn followup-request-page [request]
+ (let
+ [params (support/massage-params
+ (keywordize-keys (:params request))
+ (keywordize-keys (:form-params request))
+ #{:id})
+ id (:id (keywordize-keys params))
+ record (db/get-followuprequest db/*db* {:id id})
+ elector (if
+ record
+ (first
+ (db/search-strings-electors
+ db/*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"
+ (:session request)
+ {:title (str "Request from " (:name elector) " at " (:date visit))
+ :user (:user (:session request))
+ :visit visit
+ :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}))
+ :record record
+ :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))})))})))
+
+
+(defroutes issue-expert-routes
+ (GET "/issue-expert/list" request
+ (route/restricted (list-page request)))
+ (GET "/issue-expert/followup-request" request
+ (route/restricted (followup-request-page request)))
+ (POST "/issue-expert/followup-request" request
+ (route/restricted (followup-request-page request))))
diff --git a/src/clj/youyesyet/routes/logged_in.clj b/src/clj/youyesyet/routes/logged_in.clj
new file mode 100644
index 0000000..c334d08
--- /dev/null
+++ b/src/clj/youyesyet/routes/logged_in.clj
@@ -0,0 +1,127 @@
+(ns ^{:doc "Routes/pages available to authenticated users."
+ :author "Simon Brooke"} youyesyet.routes.logged-in
+ (:require [adl-support.core :as support]
+ [adl-support.utils :refer [safe-name]]
+ [clojure.java.io :as io]
+ [clojure.string :as s]
+ [clojure.tools.logging :as log]
+ [clojure.walk :refer [keywordize-keys]]
+ [markdown.core :refer [md-to-html-string]]
+ [noir.util.route :as route]
+ [ring.util.http-response :as response]
+ [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db]
+ [youyesyet.layout :as layout]
+ [youyesyet.oauth :as oauth]
+ [compojure.core :refer [defroutes GET POST]]
+ ))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.routes.logged-in: routes and pages for authenticated users.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(defn app-page [request]
+ (layout/render "app.html"
+ (:session request)
+ {:title "Canvasser app"}))
+
+
+(defn profile-page [request]
+ "Show the canvassers form for the current user, only."
+ (let [record (-> request :session :user)]
+ (layout/render
+ "auto/form-canvassers-Canvasser.html"
+ (:session request)
+ {:title (str "Profile for " (-> request :session :user :fullname))
+ :record record
+ :elector_id
+ (flatten
+ (remove
+ nil?
+ (list
+ (support/do-or-log-error
+ (db/get-elector db/*db* {:id (:elector_id record)})
+ :message
+ "Error while fetching elector record {:id (:elector_id record)}")
+ (support/do-or-log-error
+ (db/list-electors db/*db*)
+ :message
+ "Error while fetching elector list")))),
+ :address_id
+ (flatten
+ (remove
+ nil?
+ (list
+ (support/do-or-log-error
+ (db/get-address db/*db* {:id (:address_id record)})
+ :message
+ "Error while fetching address record {:id (:address_id record)}")
+ (support/do-or-log-error
+ (db/list-addresses db/*db*)
+ :message
+ "Error while fetching address list")))),
+ :authority_id
+ (flatten
+ (remove
+ nil?
+ (list
+ (support/do-or-log-error
+ (db/get-authority db/*db* {:id (:authority_id record)})
+ :message
+ "Error while fetching authority record {:id (:authority_id record)}")
+ (support/do-or-log-error
+ (db/list-authorities db/*db*)
+ :message
+ "Error while fetching authority list")))),
+ :roles
+ (flatten
+ (remove
+ nil?
+ (list
+ nil
+ (support/do-or-log-error
+ (db/list-roles db/*db*)
+ :message
+ "Error while fetching role list")))),
+ :expertise
+ (flatten
+ (remove
+ nil?
+ (list
+ nil
+ (support/do-or-log-error
+ (db/list-issues db/*db*)
+ :message
+ "Error while fetching issue list"))))})))
+
+
+(defn handle-logout
+ [request]
+ (dissoc (response/found "home") :user :roles))
+
+
+(defroutes logged-in-routes
+ (GET "/logout" request (handle-logout request))
+ (GET "/profile" request (route/restricted (profile-page request)))
+ (GET "/app" [request] (route/restricted (app-page request)))
+ )
diff --git a/src/clj/youyesyet/routes/manual.clj b/src/clj/youyesyet/routes/manual.clj
new file mode 100644
index 0000000..bf3cf46
--- /dev/null
+++ b/src/clj/youyesyet/routes/manual.clj
@@ -0,0 +1,38 @@
+(ns
+ youyesyet.routes.manual
+ "Manual overrides for auto-generated routes"
+ (:require
+ [noir.response :as nresponse]
+ [noir.util.route :as route]
+ [compojure.core :refer [defroutes GET POST]]
+ [ring.util.http-response :as response]
+ [clojure.java.io :as io]
+ [hugsql.core :as hugsql]
+ [youyesyet.db.core :as db]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.routes.manual: Manual overrides for auto-generated 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) 2017 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+;;; Any of the routes defined in auto.clj can be overridden in this file. This means that
+;;; new autogenerated code will not overwrite manually generated code.
diff --git a/src/clj/youyesyet/routes/oauth.clj b/src/clj/youyesyet/routes/oauth.clj
index a7c7e46..86c4b62 100644
--- a/src/clj/youyesyet/routes/oauth.clj
+++ b/src/clj/youyesyet/routes/oauth.clj
@@ -1,28 +1,33 @@
(ns ^{:doc "OAuth authentication routes - not finished, does not work yet."
:author "Simon Brooke"} youyesyet.routes.oauth
- (:require [compojure.core :refer [defroutes GET]]
+ (:require [clojure.tools.logging :as log]
+ [compojure.core :refer [defroutes GET]]
[ring.util.http-response :refer [ok found]]
[clojure.java.io :as io]
- [youyesyet.oauth :as oauth]
- [clojure.tools.logging :as log]))
+ [youyesyet.oauth :as oauth]))
(defn oauth-init
- "Initiates the Twitter OAuth"
+ "Initiates the OAuth with the authority implied by this `request`"
[request]
- (-> (oauth/fetch-request-token request)
- :oauth_token
- oauth/auth-redirect-uri
- found))
+;; (-> (oauth/fetch-request-token request)
+;; :oauth_token
+;; oauth/auth-redirect-uri
+;; found))
+ (found
+ (oauth/auth-redirect-uri
+ (:oauth_token (oauth/fetch-request-token request))
+ (:authority (:session request)))))
(defn oauth-callback
- "Handles the callback from Twitter."
+ "Handles the callback from the authority."
[request_token {:keys [session]}]
; oauth request was denied by user
(if (:denied request_token)
- (-> (found "/")
+ (-> (found "/login")
(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 request_token)]
+ (let [{:keys [user_id screen_name]}
+ (oauth/fetch-access-token request_token (:authority session))]
(log/info "successfully authenticated as" user_id screen_name)
(-> (found "/")
(assoc :session
diff --git a/src/clj/youyesyet/routes/rest.clj b/src/clj/youyesyet/routes/rest.clj
new file mode 100644
index 0000000..71aad48
--- /dev/null
+++ b/src/clj/youyesyet/routes/rest.clj
@@ -0,0 +1,91 @@
+(ns ^{:doc "Manually maintained routes which handle data transfer to/from the canvasser app."
+ :author "Simon Brooke"} youyesyet.routes.rest
+ (:require [clojure.core.memoize :as memo]
+ [clojure.java.io :as io]
+ [clojure.walk :refer [keywordize-keys]]
+ [compojure.core :refer [defroutes GET POST]]
+ [noir.response :as nresponse]
+ [noir.util.route :as route]
+ [ring.util.http-response :as response]
+ [youyesyet.locality :as l]
+ [youyesyet.db.core :as db]
+ [youyesyet.utils :refer :all]
+ ))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.routes.rest: Routes which handle data transfer to/from the
+;;;; canvasser app.
+;;;;
+;;;; 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) 2017 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;; See also src/clj/youyesyet/routes/auto-json.clj
+
+(def in-get-local-data
+ "Local data is volatile, because hopefully canvassers are updating it as they
+ work. So cache for only 90 seconds."
+ (memo/ttl
+ (fn [here]
+ (let [neighbourhood (l/neighbouring-localities here)
+ addresses (flatten
+ (map
+ #(db/list-addresses-by-locality db/*db* {:locality %})
+ neighbourhood))]
+ (map
+ (fn [a]
+ (assoc a
+ :dwellings
+ (map
+ (fn [d]
+ (assoc d
+ :electors
+ (map
+ (fn [e]
+ (assoc e
+ :intentions
+ (db/list-intentions-by-elector db/*db* {:id (:id e)})))
+ (db/list-electors-by-dwelling db/*db* {:id (:id d)}))))
+ (db/list-dwellings-by-address db/*db* {:id (:id a)}))))
+ addresses)))
+ :ttl/threshold
+ 90000))
+
+
+(defn get-local-data
+ "Get data local to the user of the canvasser app. Expects arguments `latitude` and
+ `longitude`, or `locality`. Returns a block of data for that locality"
+ [request]
+ (let
+ [{latitude :latitude longitude :longitude locality :locality}
+ (keywordize-keys (:params request))
+ here (if locality
+ (coerce-to-number locality)
+ (l/locality
+ (coerce-to-number latitude)
+ (coerce-to-number longitude)))]
+ (in-get-local-data here)))
+
+
+(defroutes rest-routes
+ (GET "/rest/get-local-data" request (route/restricted (get-local-data request)))
+;; (GET "/rest/get-issues" request (route/restricted (get-issues request)))
+;; (GET "/rest/set-intention" request (route/restricted (set-intention request)))
+;; (GET "/rest/request-followup" request (route/restricted (request-followup request))))
+)
diff --git a/src/clj/youyesyet/routes/roles.clj b/src/clj/youyesyet/routes/roles.clj
new file mode 100644
index 0000000..e2cae2b
--- /dev/null
+++ b/src/clj/youyesyet/routes/roles.clj
@@ -0,0 +1,74 @@
+(ns ^{:doc "Routes/pages available to authenticated users in specific roles."
+ :author "Simon Brooke"} youyesyet.routes.roles
+ (:require [adl-support.core :as support]
+ [adl-support.utils :refer [safe-name]]
+ [clojure.tools.logging :as log]
+ [clojure.walk :refer [keywordize-keys]]
+ [compojure.core :refer [defroutes GET POST]]
+ [noir.util.route :as route]
+ [ring.util.http-response :as response]
+ [youyesyet.config :refer [env]]
+ [youyesyet.db.core :as db-core]
+ [youyesyet.routes.issue-experts :as expert]
+ [youyesyet.layout :as layout]
+ [youyesyet.oauth :as oauth]
+ [youyesyet.routes.auto :as auto]))
+
+
+(defn roles-page [request]
+ "Render the routing page for the roles the currently logged in user is member of."
+ (let
+ [session (:session request)
+ user (:user session)
+ roles (if
+ user
+ (db-core/list-roles-by-canvasser db-core/*db* {:id (:id user)}))]
+ (log/info (str "Roles routing page; user is " user "; roles are " roles))
+ (cond
+ roles (layout/render "roles.html"
+ (:session request)
+ {:title (str "Welcome " (:fullname user) ", what do you want to do?")
+ :user user
+ :roles (map #(assoc % :link (safe-name (:name %) :sql)) roles)})
+ (empty? roles)(response/found "/app")
+ true (assoc (response/found "/login") :session (dissoc session :user)))))
+
+
+(defn admins-page
+ [request]
+ (layout/render
+ (support/resolve-template "application-index.html")
+ (:session request)
+ {:title "Administrative menu"}))
+
+
+(defn analysts-page
+ "My expectation is that analysts will do a lot of their work through QGIS or
+ some other geographical information system; so there isn't a need to put
+ anything sophisticated here."
+ [request]
+ (layout/render
+ (support/resolve-template "application-index.html")
+ (:session request)
+ {:title "Administrative menu"}))
+
+
+(defn canvassers-page
+ [request]
+ (layout/render "roles/canvasser.html" (:session request) {}))
+
+
+(defn team-organisers-page
+ [request]
+ (layout/render "roles/team-orgenisers.html" request {}))
+
+
+(defroutes roles-routes
+ (GET "/roles/admin" request (route/restricted (admins-page request)))
+ (GET "/roles/analysts" request (route/restricted (analysts-page request)))
+ (GET "/roles/canvassers" request (route/restricted (canvassers-page request)))
+ (GET "/roles/issueeditors" request (route/restricted (auto/list-issues-Issues request)))
+ (GET "/roles/issueexperts" request (route/restricted (expert/list-page request)))
+ (GET "/roles/team_organisers" request (route/restricted (auto/list-teams-Teams request)))
+ (GET "/roles" request (route/restricted (roles-page request))))
+
diff --git a/src/clj/youyesyet/routes/services.clj b/src/clj/youyesyet/routes/services.clj
index 43347c7..534ed2f 100644
--- a/src/clj/youyesyet/routes/services.clj
+++ b/src/clj/youyesyet/routes/services.clj
@@ -1,21 +1,28 @@
+;;;; This is probably the right way to do the API, but I don't understand it.
+
(ns ^{:doc "REST API."
:author "Simon Brooke"} youyesyet.routes.services
(:require [clj-http.client :as client]
- [ring.util.http-response :refer :all]
[compojure.api.sweet :refer :all]
- [schema.core :as s]))
+ [ring.util.http-response :refer :all]
+ [schema.core :as s]
+ [youyesyet.db.core :as db]))
(defapi service-routes
{:swagger {:ui "/swagger-ui"
:spec "/swagger.json"
+ :coercion :schema
:data {:info {:version "1.0.0"
:title "Sample API"
:description "Sample Services"}}}}
(context "/api" []
:tags ["thingie"]
-
- (GET "/electors/:address-id" []
+;; (GET "/electors-by-dwelling/:dwelling-id" []
+;; :return map
+;; :query-params [dwelling-id :- s/Int]
+;; :summary ""
+;; (db/list-electors-by-dwelling db/*db* {:id dwelling-id}))
(GET "/plus" []
:return Long
@@ -45,4 +52,4 @@
:return Long
:header-params [x :- Long, y :- Long]
:summary "x^y with header-parameters"
- (ok (long (Math/pow x y)))))))
+ (ok (long (Math/pow x y))))))
diff --git a/src/cljc/youyesyet/locality.cljc b/src/cljc/youyesyet/locality.cljc
new file mode 100644
index 0000000..45c9872
--- /dev/null
+++ b/src/cljc/youyesyet/locality.cljc
@@ -0,0 +1,56 @@
+(ns youyesyet.locality)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.locality: compute localities.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;; See also resources/sql/locality-trigger.sql
+
+(defn locality
+ "Compute the locality index for this `latitude`/`longitude` pair."
+ [latitude longitude]
+ (+
+ (* 1000 ;; left-shift the latitude component three digits
+ (int
+ (* latitude 100)))
+ (- ;; invert the sign of the longitude component, since
+ (int ;; we're interested in localities West of Greenwich.
+ (* longitude 100)))))
+
+(defn neighbouring-localities
+ "Return this locality with the localities immediately
+ north east, north, north west, east, west, south west,
+ south and south east of it."
+ ;; TODO: I'm not absolutely confident of my arithmetic here!
+ [locality]
+ (list
+ (- locality 99)
+ (- locality 100)
+ (- locality 101)
+ (- locality 1)
+ locality
+ (+ locality 1)
+ (+ locality 99)
+ (+ locality 100)
+ (+ locality 101)))
+
+(neighbouring-localities 5482391)
diff --git a/src/cljc/youyesyet/utils.cljc b/src/cljc/youyesyet/utils.cljc
new file mode 100644
index 0000000..fd07386
--- /dev/null
+++ b/src/cljc/youyesyet/utils.cljc
@@ -0,0 +1,37 @@
+(ns youyesyet.utils
+ #?(:clj (:require [clojure.tools.logging :as log])
+ :cljs (:require [cljs.reader :refer [read-string]])))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.utils: small utility functions.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn coerce-to-number [v]
+ "If it is possible to do so, coerce `v` to a number"
+ ;; TODO: this doesn't work in cljs.
+ (if (number? v) v
+ (try
+ (read-string (str v))
+ #?(:clj (catch Exception any
+ (log/error (str "Could not coerce '" v "' to number: " any)))
+ :cljs (catch js/Object any
+ (js/console.log (str "Could not coerce '" v "' to number: " any)))))))
diff --git a/src/cljs/youyesyet/canvasser_app/core.cljs b/src/cljs/youyesyet/canvasser_app/core.cljs
index e492a35..e93d9ab 100644
--- a/src/cljs/youyesyet/canvasser_app/core.cljs
+++ b/src/cljs/youyesyet/canvasser_app/core.cljs
@@ -10,13 +10,16 @@
[re-frame.core :as rf]
[secretary.core :as secretary]
[youyesyet.canvasser-app.ajax :refer [load-interceptors!]]
- [youyesyet.canvasser-app.handlers]
+ [youyesyet.canvasser-app.gis :refer [get-current-location]]
+ [youyesyet.canvasser-app.handlers :as h]
[youyesyet.canvasser-app.subscriptions]
[youyesyet.canvasser-app.ui-utils :as ui]
[youyesyet.canvasser-app.views.about :as about]
[youyesyet.canvasser-app.views.building :as building]
- [youyesyet.canvasser-app.views.electors :as electors]
+ [youyesyet.canvasser-app.views.dwelling :as dwelling]
+ [youyesyet.canvasser-app.views.elector :as elector]
[youyesyet.canvasser-app.views.followup :as followup]
+ [youyesyet.canvasser-app.views.gdpr :as gdpr]
[youyesyet.canvasser-app.views.issue :as issue]
[youyesyet.canvasser-app.views.issues :as issues]
[youyesyet.canvasser-app.views.map :as maps])
@@ -54,8 +57,14 @@
(defn building-page []
(building/panel))
-(defn electors-page []
- (electors/panel))
+(defn dwelling-page []
+ (dwelling/panel))
+
+(defn elector-page []
+ (elector/panel))
+
+(defn gdpr-page []
+ (gdpr/panel))
(defn followup-page []
(followup/panel))
@@ -72,8 +81,10 @@
(def pages
{:about #'about-page
:building #'building-page
- :electors #'electors-page
+ :dwelling #'dwelling-page
+ :elector #'elector-page
:followup #'followup-page
+ :gdpr #'gdpr-page
:issues #'issues-page
:issue #'issue-page
:map #'map-page
@@ -92,10 +103,12 @@
[:header
[ui/navbar]]
(if content [content]
- [:div.error (str "No content in page " :page)])
+ [:div.error (str "No content in page " @(rf/subscribe [:page]))])
[:footer
- [:div.error {:style [:display (if error "block" "none")]} (str error)]
- [:div.feedback {:style [:display (if feedback :block :none)]} (str feedback)]
+ [:div.error {:style [:display (if (empty? error) :none :block)]} (apply str error)]
+ [:div.feedback
+ {:style [:display (if (empty? feedback) :none :block)]}
+ (apply str (map #(h/feedback-messages %) (distinct feedback)))]
[:div.queue (if
(nil? outqueue) ""
(str (count outqueue) " items queued to send"))]]]))
@@ -104,35 +117,61 @@
;; Routes
(secretary/set-config! :prefix "#")
+(defn log-and-dispatch [arg]
+ (js/console.log (str "Dispatching " arg))
+ (rf/dispatch arg))
+
(secretary/defroute "/" []
- (rf/dispatch [:set-active-page :map]))
+ (log-and-dispatch [:set-active-page :map]))
(secretary/defroute "/about" []
- (rf/dispatch [:set-active-page :about]))
+ (log-and-dispatch [:set-active-page :about]))
-(secretary/defroute "/electors/:dwelling" {dwelling-id :dwelling}
- (rf/dispatch [:set-dwelling dwelling-id]))
+(secretary/defroute "/dwelling" []
+ (log-and-dispatch [:set-active-page :dwelling]))
+
+(secretary/defroute "/dwelling/:dwelling" {dwelling-id :dwelling}
+ (log-and-dispatch [:set-dwelling dwelling-id])
+ (log-and-dispatch [:set-active-page :dwelling]))
(secretary/defroute "/building/:address" {address-id :address}
- (rf/dispatch [:set-address address-id]))
+ (log-and-dispatch [:set-address address-id]))
+
+(secretary/defroute "/elector" []
+ (log-and-dispatch [:set-active-page :elector]))
+
+(secretary/defroute "/elector/:elector" {elector-id :elector}
+ (log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :elector}]))
+
+(secretary/defroute "/elector/:elector/:consent" {elector-id :elector consent :consent}
+ (log-and-dispatch [:set-consent-and-page {:elector-id elector-id :consent (and true consent) :page :elector}]))
+
+(secretary/defroute "/elector" []
+ (log-and-dispatch [:set-active-page :elector]))
(secretary/defroute "/followup" []
- (rf/dispatch [:set-active-page :followup]))
+ (log-and-dispatch [:set-active-page :followup]))
+
+(secretary/defroute "/gdpr" []
+ (log-and-dispatch [:set-active-page :gdpr]))
+
+(secretary/defroute "/gdpr/:elector" {elector-id :elector}
+ (log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :gdpr}]))
(secretary/defroute "/issues" []
- (rf/dispatch [:set-active-page :issues]))
+ (log-and-dispatch [:set-active-page :issues]))
(secretary/defroute "/issues/:elector" {elector-id :elector}
- (rf/dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}]))
+ (log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}]))
(secretary/defroute "/issue/:issue" {issue :issue}
- (rf/dispatch [:set-and-go-to-issue issue]))
+ (log-and-dispatch [:set-and-go-to-issue issue]))
(secretary/defroute "/map" []
- (rf/dispatch [:set-active-page :map]))
+ (log-and-dispatch [:set-active-page :map]))
(secretary/defroute "/set-intention/:elector/:intention" {elector-id :elector intention :intention}
- (rf/dispatch [:set-intention {:elector-id elector-id :intention intention}]))
+ (log-and-dispatch [:set-intention {:elector-id elector-id :intention intention}]))
;; -------------------------
;; History
@@ -153,6 +192,11 @@
(defn init! []
(rf/dispatch-sync [:initialize-db])
+ (get-current-location)
+ (rf/dispatch [:fetch-locality])
+ (rf/dispatch [:fetch-options])
+ (rf/dispatch [:fetch-issues])
(load-interceptors!)
(hook-browser-navigation!)
(mount-components))
+
diff --git a/src/cljs/youyesyet/canvasser_app/gis.cljs b/src/cljs/youyesyet/canvasser_app/gis.cljs
new file mode 100644
index 0000000..bca4390
--- /dev/null
+++ b/src/cljs/youyesyet/canvasser_app/gis.cljs
@@ -0,0 +1,139 @@
+(ns ^{:doc "Canvasser app map stuff."
+ :author "Simon Brooke"}
+ youyesyet.canvasser-app.gis
+ (:require [cljs.reader :refer [read-string]]
+ [cemerick.url :refer (url url-encode)]
+ [day8.re-frame.http-fx]
+ [re-frame.core :refer [dispatch reg-event-db reg-event-fx subscribe]]
+ [ajax.core :refer [GET]]
+ [ajax.json :refer [json-request-format json-response-format]]
+ [youyesyet.canvasser-app.state :as db]
+ ))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.canvasser-app.gis: stuff to do with maps.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; map stuff. If we do this in canvasser-app.views.map we get circular
+;; references, so do it here.
+
+(defn get-current-location []
+ "Get the current location from the device."
+ (try
+ (if (.-geolocation js/navigator)
+ (.getCurrentPosition
+ (.-geolocation js/navigator)
+ (fn [position]
+ (js/console.log (str "Current location is: "
+ (.-latitude (.-coords position)) ", "
+ (.-longitude (.-coords position))))
+ (dispatch [:set-latitude (.-latitude (.-coords position))])
+ (dispatch [:set-longitude (.-longitude (.-coords position))])))
+ (js/console.log "Geolocation not available"))
+ (catch js/Object any
+ (js/console.log "Exception while trying to access location: " + any))))
+
+
+(defn pin-image
+ "select the name of a suitable pin image for this address"
+ [address]
+ (let [intentions
+ (set
+ (remove
+ nil?
+ (map
+ :intention
+ (mapcat :electors
+ (:dwellings address)))))]
+ (case (count intentions)
+ 0 "unknown-pin"
+ 1 (str (name (first intentions)) "-pin")
+ "mixed-pin")))
+
+
+(defn map-pin-click-handler
+ "On clicking on the pin, navigate to the electors at the address.
+ This way of doing it adds an antry in the browser location history,
+ so back links work."
+ [id]
+ (js/console.log (str "Click handler for address #" id))
+ (let [view @(subscribe [:view])
+ centre (.getCenter view)]
+ (dispatch [:set-zoom (.getZoom view)])
+ (dispatch [:set-latitude (.-lat centre)])
+ (dispatch [:set-longitude (.-lng centre)]))
+ (set! window.location.href (str "#building/" id)))
+
+
+(defn add-map-pin
+ "Add a map-pin at this address in this map view"
+ [address view]
+ (let [lat (:latitude address)
+ lng (:longitude address)
+ pin (.icon js/L
+ (clj->js
+ {:iconAnchor [16 41]
+ :iconSize [32 42]
+ :iconUrl (str "img/map-pins/" (pin-image address) ".png")
+ :riseOnHover true
+ :shadowAnchor [16 23]
+ :shadowSize [57 24]
+ :shadowUrl "img/map-pins/shadow_pin.png"}))
+ marker (.marker js/L
+ (.latLng js/L lat lng)
+ (clj->js {:icon pin
+ :title (:address address)}))]
+ (.on (.addTo marker view) "click" (fn [_] (map-pin-click-handler (str (:id address)))))
+ marker))
+
+
+(defn map-remove-pins
+ "Remove all pins from this map `view`. Side-effecty; liable to be problematic."
+ [view]
+
+ (if
+ view
+ (.eachLayer
+ view
+ (fn [layer]
+ (try
+ (if
+ (instance? js/L.Marker layer)
+ (.removeLayer view layer))
+ (catch js/Object any (js/console.log (str "Failed to remove pin '" layer "' from map: " any)))))))
+ view)
+
+
+(defn refresh-map-pins
+ "Refresh the map pins on this map. Side-effecty; liable to be problematic."
+ []
+ (let [view (map-remove-pins @(subscribe [:view]))
+ addresses @(subscribe [:addresses])]
+ (if
+ view
+ (do
+ (js/console.log (str "Adding " (count addresses) " pins"))
+ (doall (map #(add-map-pin % view) addresses)))
+ (js/console.log "View is not yet ready"))
+ view))
+
+
diff --git a/src/cljs/youyesyet/canvasser_app/handlers.cljs b/src/cljs/youyesyet/canvasser_app/handlers.cljs
index a010699..939ae00 100644
--- a/src/cljs/youyesyet/canvasser_app/handlers.cljs
+++ b/src/cljs/youyesyet/canvasser_app/handlers.cljs
@@ -1,14 +1,20 @@
(ns ^{:doc "Canvasser app event handlers."
:author "Simon Brooke"}
youyesyet.canvasser-app.handlers
- (:require [cljs.reader :refer [read-string]]
- [re-frame.core :refer [dispatch reg-event-db]]
+ (:require [ajax.core :refer [GET]]
+ [ajax.json :refer [json-request-format json-response-format]]
+ [cemerick.url :refer (url url-encode)]
+ [cljs.reader :refer [read-string]]
+ [clojure.walk :refer [keywordize-keys]]
+ [day8.re-frame.http-fx]
+ [re-frame.core :refer [dispatch reg-event-db reg-event-fx subscribe]]
+ [youyesyet.canvasser-app.gis :refer [refresh-map-pins get-current-location]]
[youyesyet.canvasser-app.state :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
-;;;; youyesyet.handlers: handlers for events.
+;;;; youyesyet.canvasser-app.handlers: event handlers.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@@ -33,7 +39,47 @@
"Return a state like this state except with the error and feedback messages
set nil"
[state]
- (merge state {:error nil :feedback nil}))
+ (merge state {:error '() :feedback '()}))
+
+
+(def source-host (assoc
+ (url js/window.location)
+ :path "/"
+ :query nil
+ :anchor nil))
+
+
+(def feedback-messages
+ {:fetch-locality "Fetching local data."
+ :send-request "Request has been queued."
+ })
+
+
+(defn add-to-feedback
+ "Add the value of `k` in `feedback-messages` to the feedback in this `db`."
+ [db k]
+ (assoc db :feedback (cons k (:feedback db))))
+
+
+(defn remove-from-feedback
+ "Remove the value of `k` in `feedback-messages` to the feedback in this `db`."
+ [db k]
+ (assoc db
+ :feedback
+ (remove
+ #(= % k)
+ (:feedback db))))
+
+
+(defn coerce-to-number [v]
+ "If it is possible to do so, coerce `v` to a number.
+ NOTE: I tried to do this in *cljc*, but it did not work. Leave it alone."
+ (if (number? v) v
+ (try
+ (read-string (str v))
+ (catch js/Object any
+ (js/console.log (str "Could not coerce '" v "' to number: " any))
+ v))))
(defn get-elector
@@ -42,12 +88,19 @@
([elector-id state]
(get-elector elector-id state (:address state)))
([elector-id state address]
- (first
- (remove
- nil?
- (map
- #(if (= elector-id (:id %)) %)
- (:electors address))))))
+ (try
+ (first
+ (remove
+ nil?
+ (map
+ #(if (= (coerce-to-number elector-id) (:id %)) %)
+ (:electors state))))
+ (catch js/Object _
+ (str
+ "Failed to find id '"
+ elector-id
+ "' among '"
+ (:electors state) "'")))))
(reg-event-db
@@ -56,6 +109,158 @@
db/default-db))
+;; (reg-event-fx
+;; :feedback
+;; (fn [x y]
+;; (js/console.log (str "Feedback event called with x = " x "; y = " y))
+;; (:db x)))
+
+
+;; (reg-event-fx
+;; :issues
+;; (fn [x y]
+;; (js/console.log (str "Issues event called with x = " x "; y = " y))
+;; (:db x)))
+
+
+;; (reg-event-fx
+;; :options
+;; (fn [x y]
+;; (js/console.log (str "Options event called with x = " x "; y = " y))
+;; (:db x)))
+
+
+;; (reg-event-fx
+;; :event
+;; (fn [x y]
+;; (js/console.log (str "Event event called with x = " x "; y = " y))
+;; (:db x)))
+
+
+(reg-event-fx
+ :fetch-locality
+ (fn [{db :db} _]
+ (js/console.log "Fetching locality data")
+ ;; we return a map of (side) effects
+ {:http-xhrio {:method :get
+ :uri (str source-host
+ "rest/get-local-data?latitude="
+ (:latitude db)
+ "&longitude="
+ (:longitude db))
+ :format (json-request-format)
+ :response-format (json-response-format {:keywords? true})
+ :on-success [:process-locality]
+ :on-failure [:bad-locality]}
+ :db (add-to-feedback db :fetch-locality)}))
+
+
+(reg-event-db
+ :get-current-location
+ (fn [db _]
+ (js/console.log "Updating current location")
+ (assoc db :froboz (get-current-location))))
+
+
+(reg-event-db
+ :process-locality
+ (fn
+ [db [_ response]]
+ (js/console.log "Updating locality data")
+ ;; loop to do it again
+ (dispatch [:dispatch-later [{:ms 5000 :dispatch [:fetch-locality]}
+ {:ms 1000 :dispatch [:get-current-location]}]])
+ (assoc
+ (remove-from-feedback db :fetch-locality)
+ (refresh-map-pins)
+ :addresses (js->clj response))))
+
+
+(reg-event-db
+ :bad-locality
+ (fn [db _]
+ ;; TODO: signal something has failed? It doesn't matter very much, unless it keeps failing.
+ (js/console.log "Failed to fetch locality data")
+ ;; loop to do it again
+ (dispatch [:dispatch-later [{:ms 60000 :dispatch [:fetch-locality]}
+ {:ms 1000 :dispatch [:get-current-location]}]])
+ (assoc
+ (remove-from-feedback db :fetch-locality)
+ :error (cons :fetch-locality (:error db)))))
+
+
+(reg-event-fx
+ :fetch-options
+ (fn [{db :db} _]
+ (js/console.log "Fetching options")
+ ;; we return a map of (side) effects
+ {:http-xhrio {:method :get
+ :uri (str source-host "json/auto/list-options")
+ :format (json-request-format)
+ :response-format (json-response-format {:keywords? true})
+ :on-success [:process-options]
+ :on-failure [:bad-options]}
+ :db (add-to-feedback db :fetch-options)}))
+
+
+(reg-event-db
+ :process-options
+ (fn
+ [db [_ response]]
+ (let [options (js->clj response)]
+ (js/console.log (str "Updating options: " options))
+ (assoc
+ (remove-from-feedback db :fetch-options)
+ :options options))))
+
+
+(reg-event-db
+ :bad-options
+ (fn [db _]
+ (js/console.log "Failed to fetch options")
+ (assoc
+ (remove-from-feedback db :fetch-options)
+ :error (cons :fetch-options (:error db)))))
+
+
+(reg-event-fx
+ :fetch-issues
+ (fn [{db :db} _]
+ (js/console.log "Fetching issues")
+ ;; we return a map of (side) effects
+ {:http-xhrio {:method :get
+ :uri (str source-host "json/auto/list-issues")
+ :format (json-request-format)
+ :response-format (json-response-format {:keywords? true})
+ :on-success [:process-issues]
+ :on-failure [:bad-issues]}
+ :db (add-to-feedback db :fetch-issues)}))
+
+
+(reg-event-db
+ :process-issues
+ (fn
+ [db [_ response]]
+ (let [issues (reduce
+ merge {}
+ (map
+ #(hash-map (keyword (:id %)) %)
+ (js->clj response)))]
+ (js/console.log (str "Updating issues: " issues))
+ (assoc
+ (remove-from-feedback db :fetch-issues)
+ :issues issues))))
+
+
+(reg-event-db
+ :bad-issues
+ (fn [db _]
+ (js/console.log "Failed to fetch issues")
+ (assoc
+ (remove-from-feedback db :fetch-issues)
+ :error (cons :fetch-issues (:error db)))))
+
+
(reg-event-db
:send-intention
(fn [db [_ args]]
@@ -87,11 +292,11 @@
(:dwellings old-address))))]
(cond
(nil? old-elector)
- (assoc db :error "No elector found; not setting intention")
- (= intention (:intention old-elector))
- (do
- (js/console.log "Elector's intention hasn't changed; not setting intention")
- db)
+ (assoc db :error (cons "No elector found; not setting intention" (:error db))
+ (= intention (:intention old-elector))
+ (do
+ (js/console.log "Elector's intention hasn't changed; not setting intention")
+ db))
true
(do
(js/console.log (str "Setting intention of elector " old-elector " to " intention))
@@ -115,8 +320,7 @@
(if (and (:elector db) (:issue db) (:telephone db))
(do
(js/console.log "Sending request")
- (assoc db
- :feedback "Request has been queued"
+ (assoc (add-to-feedback db :send-request)
:outqueue (cons
{:elector-id (:id (:elector db))
:issue (:issue db)
@@ -126,53 +330,72 @@
(reg-event-db
:set-active-page
- (fn [db [_ page]]
- (if page
- (assoc (clear-messages db) :page page))))
+ (fn [db [_ k]]
+ (if k
+ (assoc (clear-messages db) :page k)
+ db)))
(reg-event-db
:set-address
(fn [db [_ address-id]]
- (let [id (read-string address-id)
+ (let [id (coerce-to-number address-id)
address (first (remove nil? (map #(if (= id (:id %)) %) (:addresses db))))]
(if
(= (count (:dwellings address)) 1)
(assoc (clear-messages db)
:address address
:dwelling (first (:dwellings address))
- :page :electors)
+ :electors (:electors (first (:dwellings address)))
+ :page :dwelling)
(assoc (clear-messages db)
:address address
:dwelling nil
+ :electors nil
:page :building)))))
+(reg-event-db
+ :set-consent-and-page
+ (fn [db [_ args]]
+ (let [page (:page args)
+ consent (:consent args)
+ elector-id (coerce-to-number (:elector-id args))
+ elector (get-elector elector-id db)]
+ (js/console.log (str "Setting page to " page ", consent to " consent " for " (:name elector)))
+ (assoc (clear-messages db) :elector (assoc elector :consent true) :page page))))
+
+
(reg-event-db
:set-dwelling
(fn [db [_ dwelling-id]]
- (let [id (read-string dwelling-id)
+ (let [id (coerce-to-number dwelling-id)
dwelling (first
(remove
nil?
(map
#(if (= id (:id %)) %)
(mapcat :dwellings (:addresses db)))))]
- (assoc (clear-messages db) :dwelling dwelling :page :electors))))
+ (if dwelling
+ (assoc
+ (clear-messages db)
+ :dwelling dwelling
+ :electors (:electors dwelling)
+ :page :dwelling)))))
(reg-event-db
:set-and-go-to-issue
(fn [db [_ issue]]
- (js/console.log (str "Setting page to :issue, issue to " issue))
- (assoc (assoc (clear-messages db) :issue issue) :page :issue)))
+ (js/console.log (str "Setting page to :issue, issue to " issue ", issues are " (:issues db)))
+ (assoc (assoc (clear-messages db) :issue (keyword issue)) :page :issue)))
- (reg-event-db
+(reg-event-db
:set-elector-and-page
(fn [db [_ args]]
(let [page (:page args)
- elector-id (read-string (:elector-id args))
+ elector-id (:elector-id args)
elector (get-elector elector-id db)]
(js/console.log (str "Setting page to " page ", elector to " elector))
(assoc (clear-messages db) :elector elector :page page))))
@@ -181,7 +404,7 @@
(reg-event-db
:set-elector
(fn [db [_ elector-id]]
- (let [elector (get-elector (read-string elector-id) db)]
+ (let [elector (get-elector (coerce-to-number elector-id) db)]
(js/console.log (str "Setting elector to " elector))
(assoc (clear-messages db) :elector elector))))
@@ -190,19 +413,19 @@
:set-issue
(fn [db [_ issue]]
(js/console.log (str "Setting issue to " issue))
- (assoc (clear-messages db) :issue issue)))
+ (assoc (clear-messages db) :issue (keyword issue))))
(reg-event-db
:set-latitude
- (fn [db [_ issue]]
- (assoc db :latitude issue)))
+ (fn [db [_ v]]
+ (assoc db :latitude (coerce-to-number v))))
(reg-event-db
:set-longitude
- (fn [db [_ issue]]
- (assoc db :longitude issue)))
+ (fn [db [_ v]]
+ (assoc db :longitude (coerce-to-number v))))
(reg-event-db
diff --git a/src/cljs/youyesyet/canvasser_app/state.cljs b/src/cljs/youyesyet/canvasser_app/state.cljs
index d62be6b..cb67e57 100644
--- a/src/cljs/youyesyet/canvasser_app/state.cljs
+++ b/src/cljs/youyesyet/canvasser_app/state.cljs
@@ -27,63 +27,130 @@
;;; This is the constructor for the atom in which the state of the user interface is held.
;;; The atom gets updated by 'events' registered in handler.cljs, q.v.
-;;;
-;;; not wonderfully happy with 'db' as a name for this namespace; will probably change to
-;;; 'client-state'.
(def default-db
- {;;; the currently selected address, if any.
- :address {:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2569057
- :dwellings [{:id 1
- :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
- {:id 2 :name "Ann Anderson" :gender :female}
- {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
- {:id 4 :name "Andy Anderson" :intention :yes}]}]}
- ;;; a list of the addresses in the current location at which there
- ;;; are electors registered.
- :addresses [{:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2569057
- :dwellings [{:id 1
- :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
- {:id 2 :name "Ann Anderson" :gender :female}
- {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
- {:id 4 :name "Andy Anderson" :intention :yes}]}]}
- {:id 2 :address "15 Imaginary Terrace, IM1 3TE" :latitude 55.8252354 :longitude -4.2569077
- :dwellings [{:id 2
- :electors [{:id 1 :name "Beryl Brown" :gender :female}
- {:id 2 :name "Betty Black" :gender :female}]}]}
+ {
+ :addresses
+ [{:locality 548223905,
+ :address
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF",
+ :phone nil,
+ :postcode "DG7 1RF",
+ :longitude -3.905045374625994,
+ :district_id 1,
+ :dwellings
+ [{:address_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF",
+ :address_id 18,
+ :sub_address "",
+ :id 17,
+ :id_2 17,
+ :address_id_2 18,
+ :sub_address_2 "",
+ :electors
+ [{:email nil,
+ :dwelling_id_2 17,
+ :dwelling_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF, ",
+ :intentions
+ [{:locality 548223905,
+ :visit_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF, 2018-06-14 20:29:34.721522",
+ :option_id_expanded "Yes",
+ :option_id "Yes",
+ :option_id_2 "Yes",
+ :visit_id_2 1,
+ :elector_id_2 61,
+ :visit_id 1,
+ :elector_id 61,
+ :id 1,
+ :elector_id_expanded nil,
+ :id_2 1}],
+ :phone nil,
+ :phone_2 nil,
+ :gender_expanded "Female",
+ :name "Alice Sutherland",
+ :dwelling_id 17,
+ :id 61,
+ :gender "Female",
+ :gender_2 "Female",
+ :name_2 "Alice Sutherland",
+ :email_2 nil,
+ :id_2 61}
+ {:email nil,
+ :dwelling_id_2 17,
+ :dwelling_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF, ",
+ :intentions [],
+ :phone nil,
+ :phone_2 nil,
+ :gender_expanded "Female",
+ :name "Charlie Sutherland",
+ :dwelling_id 17,
+ :id 62,
+ :gender "Female",
+ :gender_2 "Female",
+ :name_2 "Charlie Sutherland",
+ :email_2 nil,
+ :id_2 62}
+ {:email nil,
+ :dwelling_id_2 17,
+ :dwelling_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF, ",
+ :intentions [],
+ :phone nil,
+ :phone_2 nil,
+ :gender_expanded "Male",
+ :name "Keith Sutherland",
+ :dwelling_id 17,
+ :id 64,
+ :gender "Male",
+ :gender_2 "Male",
+ :name_2 "Keith Sutherland",
+ :email_2 nil,
+ :id_2 64}
+ {:email nil,
+ :dwelling_id_2 17,
+ :dwelling_id_expanded
+ "HAZELFIELD HOUSE, CASTLE DOUGLAS, DG7 1RF, DG7 1RF, ",
+ :intentions [],
+ :phone nil,
+ :phone_2 nil,
+ :gender_expanded "Female",
+ :name "Lucy Sutherland",
+ :dwelling_id 17,
+ :id 63,
+ :gender "Female",
+ :gender_2 "Female",
+ :name_2 "Lucy Sutherland",
+ :email_2 nil,
+ :id_2 63}]}],
+ :id 18,
+ :latitude 54.8222716877376}]
- {:id 3 :address "17 Imaginary Terrace, IM1 3TE" :latitude 55.825166 :longitude -4.256926
- :dwellings [{:id 3 :sub-address "Flat 1"
- :electors [{:id 1 :name "Catriona Crathie" :gender :female :intention :yes}
- {:id 2 :name "Colin Caruthers" :gender :male :intention :yes}
- {:id 3 :name "Calum Crathie" :intention :yes}]}
- {:id 4 :sub-address "Flat 2"
- :electors [{:id 1 :name "David Dewar" :gender :male :intention :no}]}]}]
+ ;;; the currently selected address, if any.
+ :address nil
;;; electors at the currently selected dwelling
- :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
- {:id 2 :name "Ann Anderson" :gender :female}
- {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
- {:id 4 :name "Andy Anderson" :intention :yes}]
+ :electors nil
;;; any error to display
:error nil
;;; the issue from among the issues which is currently selected.
;;; any confirmation message to display
- :feedback nil
+ :feedback '()
;;; the currently selected issue
- :issue "Currency"
+ :issue nil
;;; the issues selected for the issues page on this day.
- :issues {"Currency" "Scotland could keep the Pound, or use the Euro. But we could also set up a new currency of our own. Yada yada yada"
- "Monarchy" "Scotland could keep the Queen. This is an issue to be decided after independence. Yada yada yada"
- "Defence" "Scotland will not have nuclear weapons, and will probably not choose to engage in far-off wars. But we could remain members of NATO"}
+ :issues nil
;;; message of the day
:motd "This is a test version only. There is no real data."
;;; the options from among which electors can select.
- :options [{:id :yes :description "Yes"} {:id :no :description "No"}]
+ :options nil
;;; the queue of items waiting to be transmitted.
:outqueue ()
;;; the currently displayed page within the app.
:page :home
:view nil
- :latitude 55.82
- :longitude -4.25
+ :latitude 54.82
+ :longitude -3.92
:zoom 12})
+
diff --git a/src/cljs/youyesyet/canvasser_app/ui_utils.cljs b/src/cljs/youyesyet/canvasser_app/ui_utils.cljs
index 068bad6..12ab66f 100644
--- a/src/cljs/youyesyet/canvasser_app/ui_utils.cljs
+++ b/src/cljs/youyesyet/canvasser_app/ui_utils.cljs
@@ -28,20 +28,30 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defn back-link []
- [:div.back-link-container {:id "back-link-container"}
- [:a {:href "javascript:history.back()" :id "back-link"} "Back"]])
+(defn back-link
+ "Generate a back link to the preceding page, or, if `target` is specified,
+ to a particular page."
+ ([]
+ (back-link "javascript:history.back()"))
+ ([target]
+ [:div.back-link-container {:key (gensym "back-link")}
+ [:a.back-link {:href target} "Back"]]))
-(defn big-link [text target]
- [:div.big-link-container {:key target}
- [:a.big-link {:href target} text]])
+(defn big-link
+ [text & {:keys [target handler]}]
+ [:div.big-link-container {:key (gensym "big-link")}
+ [:a.big-link (merge
+ (if target {:href target}{})
+ (if handler {:on-click handler}{}))
+ text]])
(defn nav-link [uri title page collapsed?]
- (let [selected-page (rf/subscribe [:page])]
+ (let [selected-page @(rf/subscribe [:page])]
[:li.nav-item
- {:class (when (= page @selected-page) "active")}
+ {:class (when (= page selected-page) "active")
+ :key (gensym "nav-link")}
[:a.nav-link
{:href uri
:on-click #(reset! collapsed? true)} title]]))
@@ -63,6 +73,6 @@
:on-click #(swap! collapsed? not)}]
[:menu.nav {:id "nav-menu" :class (if @collapsed? "hidden" "shown")}
(nav-link "#/map" "Map" :map collapsed?)
- (nav-link "#/electors" "Electors" :electors collapsed?)
+ (nav-link "#/dwelling" "Electors" :dwelling collapsed?)
(nav-link "#/issues" "Issues" :issues collapsed?)
(nav-link "#/about" "About" :about collapsed?)]]))
diff --git a/src/cljs/youyesyet/canvasser_app/views/about.cljs b/src/cljs/youyesyet/canvasser_app/views/about.cljs
index 6b4d9ca..27c910b 100644
--- a/src/cljs/youyesyet/canvasser_app/views/about.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/about.cljs
@@ -39,7 +39,7 @@
[:div
[:h1 "You Yes Yet?"]
[:div.container {:id "main-container"}
- [:h2 "Pre-alpha/proof of concept"]
+ [:h2 "Alpha test code"]
[:p.motd {:dangerouslySetInnerHTML
{:__html (md->html motd)}}]
[:p
diff --git a/src/cljs/youyesyet/canvasser_app/views/building.cljs b/src/cljs/youyesyet/canvasser_app/views/building.cljs
index 54a1ed2..5396b2e 100644
--- a/src/cljs/youyesyet/canvasser_app/views/building.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/building.cljs
@@ -48,7 +48,7 @@
[dwelling]
(ui/big-link
(:sub-address dwelling)
- (str "#/electors/" (:id dwelling))) )
- (sort
- #(< (:sub-address %1) (:sub-address %2))
+ :target (str "#/dwelling/" (:id dwelling))) )
+ (sort-by
+ :sub-address
(:dwellings address)))]]]))
diff --git a/src/cljs/youyesyet/canvasser_app/views/electors.cljs b/src/cljs/youyesyet/canvasser_app/views/dwelling.cljs
similarity index 61%
rename from src/cljs/youyesyet/canvasser_app/views/electors.cljs
rename to src/cljs/youyesyet/canvasser_app/views/dwelling.cljs
index 482d21a..73f28b3 100644
--- a/src/cljs/youyesyet/canvasser_app/views/electors.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/dwelling.cljs
@@ -1,13 +1,13 @@
(ns ^{:doc "Canvasser app electors in household panel."
:author "Simon Brooke"}
- youyesyet.canvasser-app.views.electors
+ youyesyet.canvasser-app.views.dwelling
(:require [reagent.core :refer [atom]]
[re-frame.core :refer [reg-sub subscribe dispatch]]
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
-;;;; youyesyet.canvasser-app.views.electors: electors view for youyesyet.
+;;;; youyesyet.canvasser-app.views.dwelling: dweling view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@@ -40,15 +40,20 @@
;;; Each column contains
;;; 1. a stick figure identifying gender (for recognition);
;;; 2. the elector's name;
-;;; 3. one icon for each option on the ballot;
-;;; 4. an 'issues' icon.
;;; The mechanics of how this panel is laid out don't matter.
+(defn go-to-gdpr-for-elector [elector]
+ (dispatch [:set-elector-and-page {:elector-id (:id elector)
+ :page :gdpr}]))
+
+
(defn gender-cell
[elector]
(let [gender (:gender elector)
- image (if gender (name gender) "unknown")]
- [:td {:key (:id elector)} [:img {:src (str "img/gender/" image ".png") :alt image}]]))
+ image (if gender (name gender) "Unknown")]
+ [:td {:key (str "gender-" (:id elector))}
+ [:a {:href (str "#gdpr/" (:id elector))}
+ [:img {:src (str "img/gender/" image ".png") :alt image}]]]))
(defn genders-row
@@ -60,7 +65,10 @@
(defn name-cell
[elector]
- [:td {:key (str "name-" (:id elector))} (:name elector)])
+ [:td {:key (str "name-" (:id elector))
+ :on-click #(go-to-gdpr-for-elector elector)}
+ (:name elector)])
+
(defn names-row
[electors]
@@ -69,41 +77,6 @@
#(name-cell %) electors)])
-(defn options-row
- [electors option]
- (let [optid (:id option)
- optname (name optid)]
- [:tr {:key (str "options-" optname)}
- (map
- (fn [elector] (let [selected (= optid (:intention elector))
- image (if selected (str "img/option/" optname "-selected.png")
- (str "img/option/" optname "-unselected.png"))]
- [:td {:key (str "option-" optid "-" (:id elector))}
- [:img
- {:src image
- :alt optname
- :on-click #(dispatch
- [:send-intention {:elector-id (:id elector)
- :intention optid}])}]]))
- ;; TODO: impose an ordering on electors - by name or by id
- electors)]))
-
-
-(defn issue-cell
- "Create an issue cell for a particular elector"
- [elector]
- [:td {:key (:id elector)}
- [:a {:href (str "#/issues/" (:id elector))}
- [:img {:src "img/issues.png" :alt "Issues"}]]])
-
-
-(defn issues-row
- [electors]
- [:tr
- (map
- #(issue-cell %)
- electors)])
-
(defn panel
"Generate the electors panel."
[]
@@ -120,16 +93,8 @@
[:div.container {:id "main-container"}
[:table
[:tbody
- ;; genders row
(genders-row electors)
- ;; names row
- (names-row electors)
- ;; options rows
- (map
- #(options-row electors %)
- options)
- ;; issues row
- (issues-row electors)]]
+ (names-row electors)]]
(ui/back-link)]]
(ui/error-panel "No address selected"))))
diff --git a/src/cljs/youyesyet/canvasser_app/views/elector.cljs b/src/cljs/youyesyet/canvasser_app/views/elector.cljs
new file mode 100644
index 0000000..119ac44
--- /dev/null
+++ b/src/cljs/youyesyet/canvasser_app/views/elector.cljs
@@ -0,0 +1,93 @@
+(ns ^{:doc "Canvasser app single elector panel."
+ :author "Simon Brooke"}
+ youyesyet.canvasser-app.views.elector
+ (:require [reagent.core :refer [atom]]
+ [re-frame.core :refer [reg-sub subscribe dispatch]]
+ [youyesyet.canvasser-app.ui-utils :as ui]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.canvasser-app.views.elector: elector view for youyesyet.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(defn gender-row
+ "Generate a row containing a cell showing the gender of this `elector`."
+ [elector]
+ (let [gender (:gender elector)
+ image (if gender (name gender) "unknown")]
+ [:tr
+ [:td {:key (:id elector)}
+ [:img {:src (str "img/gender/" image ".png") :alt image}]]]))
+
+
+(defn name-row
+ "Generate a row containing a cell showing the name of this `elector`."
+ [elector]
+ [:tr
+ [:td {:key (:id elector)}
+ (:name elector)]])
+
+
+(defn option-row
+ "Generate a row showing this `option` for this elector."
+ [elector option]
+ (let [optid (:id option)
+ optname (name optid)]
+ [:tr {:key (str "options-" optname)}
+ (let [selected (= optid (:intention elector))
+ image (if selected (str "img/option/" optname "-selected.png")
+ (str "img/option/" optname "-unselected.png"))]
+ [:td {:key (str "option-" optid "-" (:id elector))}
+ [:img
+ {:src image
+ :alt optname
+ :on-click #(dispatch
+ [:send-intention {:elector-id (:id elector)
+ :intention optid}])}]])]))
+
+
+(defn issue-row
+ "Generate a row containing an issue cell for a particular elector"
+ [elector]
+ [:tr
+ [:td {:key (:id elector)}
+ [:a {:href (str "#issues/" (:id elector))}
+ [:img {:src "img/issues.png" :alt "Issues"}]]]])
+
+
+(defn panel
+ "Generate the elector panel."
+ []
+ (let [elector @(subscribe [:elector])
+ options @(subscribe [:options])]
+ (if elector
+ [:div
+ [:h1 (:name elector)]
+ [:div.container {:id "main-container"}
+ [:table
+ [:tbody
+ (map
+ #(option-row elector %)
+ options)
+ (issue-row elector)]]
+ (ui/back-link "#dwelling")]]
+ (ui/error-panel "No elector selected"))))
diff --git a/src/cljs/youyesyet/canvasser_app/views/gdpr.cljs b/src/cljs/youyesyet/canvasser_app/views/gdpr.cljs
new file mode 100644
index 0000000..65e1e5a
--- /dev/null
+++ b/src/cljs/youyesyet/canvasser_app/views/gdpr.cljs
@@ -0,0 +1,71 @@
+(ns ^{:doc "Canvasser app electors in household panel."
+ :author "Simon Brooke"}
+ youyesyet.canvasser-app.views.gdpr
+ (:require [re-frame.core :refer [reg-sub subscribe dispatch]]
+ [reagent.core :as reagent]
+ [youyesyet.canvasser-app.ui-utils :as ui]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; youyesyet.canvasser-app.views.gdpr: consent form.
+;;;;
+;;;; 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) 2016 Simon Brooke for Radical Independence Campaign
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; OK, the idea here is a GDPR consent form to be signed by the elector
+
+(defn gdpr-render
+ []
+ (let [elector @(subscribe [:elector])]
+ [:div
+ [:h1 "I, " (:name elector)]
+ [:div.container {:id "main-container"}
+ [:table
+ [:tbody
+ [:tr
+ [:td
+ [:p "Consent to have data about my voting intention stored by "
+ [:b "Project Hope"]
+ " for use in the current referendum campaign, after which
+ it will be anonymised or deleted."]
+ [:p [:i "If you do not consent, we will store your voting intention
+ only against your electoral district, and not link it to you"]]]]
+ [:tr
+ [:td
+ [:canvas {:id "signature-pad"}]]]]]]
+ (ui/back-link "#dwelling")
+ (ui/big-link "I consent"
+ :target (str "#elector/" (:id elector) "/true")
+ :handler #(fn [] (dispatch [:set-consent-and-page {:elector-id (:id elector) :page :elector}])))
+ ;; TODO: need to save the signature
+ (ui/big-link "I DO NOT consent"
+ :target (str "#elector/" (:id elector) "/true"))]))
+ ;; :handler #(fn [] (dispatch [:set-elector-and-page {:elector-id (:id elector) :page :elector}])))]))
+
+
+(defn gdpr-did-mount
+ []
+ (js/SignaturePad. (.getElementById js/document "signature-pad")))
+
+
+(defn panel
+ "A reagent class for the GDPR consent form"
+ []
+ (reagent/create-class {:reagent-render gdpr-render
+ :component-did-mount gdpr-did-mount}))
diff --git a/src/cljs/youyesyet/canvasser_app/views/issue.cljs b/src/cljs/youyesyet/canvasser_app/views/issue.cljs
index 34afe10..9a75a43 100644
--- a/src/cljs/youyesyet/canvasser_app/views/issue.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/issue.cljs
@@ -38,14 +38,16 @@
(defn panel
"Generate the issue panel."
[]
- (let [issue @(subscribe [:issue])
- issues @(subscribe [:issues])]
+ (let [id @(subscribe [:issue])
+ issues @(subscribe [:issues])
+ issue (id issues)]
+ (js/console.log (str "Id: " id "; issue: " issue))
[:div
- [:h1 issue]
+ [:h1 (:id issue)]
[:div.container {:id "main-container"}
[:div {:id "issue"}
[:div {:id "issue-text"
:dangerouslySetInnerHTML
- {:__html (md->html (issues issue))}}]]
- (ui/big-link "Request call" "#/followup")
+ {:__html (md->html (:brief issue))}}]]
+ (ui/big-link "Request call" :target "#/followup")
(ui/back-link)]]))
diff --git a/src/cljs/youyesyet/canvasser_app/views/issues.cljs b/src/cljs/youyesyet/canvasser_app/views/issues.cljs
index 8b2ca47..eb0a6fb 100644
--- a/src/cljs/youyesyet/canvasser_app/views/issues.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/issues.cljs
@@ -40,11 +40,12 @@
"Generate the issues panel."
[]
(let [issues @(subscribe [:issues])]
+ (js/console.log (str "Issues: " issues))
(if issues
[:div
[:h1 "Issues"]
[:div.container {:id "main-container"}
(ui/back-link)
[:div {:id "issue-list"}
- (map (fn [k] (ui/big-link k (str "#/issue/" k))) (keys issues))]]]
+ (map (fn [i] (ui/big-link (:id i) :target (str "#issue/" (:id i)))) (vals issues))]]]
(ui/error-panel "No issues loaded"))))
diff --git a/src/cljs/youyesyet/canvasser_app/views/map.cljs b/src/cljs/youyesyet/canvasser_app/views/map.cljs
index 257e404..5aa8c40 100644
--- a/src/cljs/youyesyet/canvasser_app/views/map.cljs
+++ b/src/cljs/youyesyet/canvasser_app/views/map.cljs
@@ -1,8 +1,9 @@
(ns ^{:doc "Canvasser app map view panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.map
- (:require [re-frame.core :refer [reg-sub subscribe dispatch]]
- [reagent.core :as reagent]))
+ (:require [re-frame.core :refer [reg-sub subscribe dispatch dispatch-sync]]
+ [reagent.core :as reagent]
+ [youyesyet.canvasser-app.gis :refer [refresh-map-pins get-current-location]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@@ -27,7 +28,6 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have
;;; one source file/namespace per view. Each namespace contains a function 'panel'
;;; whose output is an enlive-style specification of the view to be redered.
@@ -53,102 +53,46 @@
(def osm-url "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png")
(def osm-attrib "Map data ©
OpenStreetMap contributors")
-
-(defn pin-image
- "select the name of a suitable pin image for this address"
- [address]
- (let [intentions
- (set
- (remove
- nil?
- (map
- :intention
- (mapcat :electors
- (:dwellings address)))))]
- (case (count intentions)
- 0 "unknown-pin"
- 1 (str (name (first intentions)) "-pin")
- "mixed-pin")))
-
-
-(defn map-pin-click-handler
- "On clicking on the pin, navigate to the electors at the address.
- This way of doing it adds an antry in the browser location history,
- so back links work."
- [id]
- (js/console.log (str "Click handler for address #" id))
- (let [view @(subscribe [:view])
- centre (.getCenter view)]
- (dispatch [:set-zoom (.getZoom view)])
- (dispatch [:set-latitude (.-lat centre)])
- (dispatch [:set-longitude (.-lng centre)]))
- (set! window.location.href (str "#building/" id)))
-;; This way is probably more idiomatic React, but history doesn't work:
-;; (defn map-pin-click-handler
-;; [id]
-;; (dispatch [:set-address id]))
-
-
-(defn add-map-pin
- "Add a map-pin at this address in this map view"
- [address view]
- (let [lat (:latitude address)
- lng (:longitude address)
- pin (.icon js/L
- (clj->js
- {:iconUrl (str "img/map-pins/" (pin-image address) ".png")
- :shadowUrl "img/map-pins/shadow_pin.png"
- :iconSize [32 42]
- :shadowSize [57 24]
- :iconAnchor [16 41]
- :shadowAnchor [16 23]}))
- marker (.marker js/L
- (.latLng js/L lat lng)
- (clj->js {:icon pin
- :title (:address address)}))
- ]
- (.on marker "click" (fn [_] (map-pin-click-handler (str (:id address)))))
- (.addTo marker view)))
-
-
;; My gods mapbox is user-hostile!
(defn map-did-mount-mapbox
"Did-mount function loading map tile data from MapBox (proprietary)."
[]
+ (get-current-location)
(let [view (.setView (.map js/L "map" (clj->js {:zoomControl "false"})) #js [55.82 -4.25] 40)]
;; NEED TO REPLACE FIXME with your mapID!
(.addTo (.tileLayer js/L "http://{s}.tiles.mapbox.com/v3/FIXME/{z}/{x}/{y}.png"
(clj->js {:attribution "Map data © [...]"
- :maxZoom 18}))
- view)))
+ :maxZoom 18})))
+ view))
(defn map-did-mount-osm
"Did-mount function loading map tile data from Open Street Map."
[]
+ (get-current-location)
(let [view (.setView
- (.map js/L "map" (clj->js {:zoomControl false}))
+ (.map js/L
+ "map"
+ (clj->js {:zoomControl false}))
#js [@(subscribe [:latitude]) @(subscribe [:longitude])]
- @(subscribe [:zoom]))
- addresses @(subscribe [:addresses])]
- (js/console.log (str "Adding " (count addresses) " pins"))
- (doall (map #(add-map-pin % view) addresses))
+ @(subscribe [:zoom]))]
(.addTo (.tileLayer js/L osm-url
(clj->js {:attribution osm-attrib
:maxZoom 18}))
view)
- (dispatch [:set-view view])
+ (dispatch-sync [:set-view view])
+ (refresh-map-pins)
view))
(defn map-did-mount
"Select the actual map provider to use."
[]
- (case *map-provider*
- :mapbox (map-did-mount-mapbox)
- :osm (map-did-mount-osm))
- ;; potentially others
- )
+ (dispatch-sync [:set-view (case *map-provider*
+ :mapbox (map-did-mount-mapbox)
+ :osm (map-did-mount-osm)
+ ;; potentially others
+ )]))
(defn map-render
diff --git a/youyesyet.adl.xml b/youyesyet.adl.xml
new file mode 100644
index 0000000..dd16842
--- /dev/null
+++ b/youyesyet.adl.xml
@@ -0,0 +1,1094 @@
+
+
+
+
+ A web-app intended to be used by canvassers
+ campaigning for a 'Yes' vote in the second independence
+ referendum. The web-app will be delivered to canvassers out
+ knocking doors primarily through an HTML5/React single-page app
+ designed to work on a mobile phone; it's possible that someone
+ else may do an Android of iPhone native app to address the same
+ back end but at present I have no plans for this. There must also
+ be an administrative interface through which privileged users can
+ set the system up and authorise canvassers, and a 'followup'
+ interface through which issue-expert specialist canvassers can
+ address particular electors' queries.
+
+ See
+ https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/488478/Bulk_Data_Transfer_-_additional_validation_valid_from_12_November_2015.pdf,
+ section 3
+ A valid postcode.
+
+
+ All users
+
+
+ All users of the canvasser app Able to read and
+ add canvassing data in a limited radius around their current
+ position.
+
+
+ Organisers of canvassing teams Able to see and
+ modify data on the canvassers in the team(s) they organise;
+ able to add canvassers to their team; able to update canvassers
+ in their team, including resetting passwords and locking
+ accounts; able to see canvass data over the whole area in which
+ their team operates.
+
+
+ People expert on particular issues. Able to read
+ followup requests, and the electors to which they relate; able
+ to access (read/write) the issues wiki; able to write followuop
+ action records.
+
+
+ Users entitled to see an overview of the
+ canvassing data collected. Able to read canvassing data over
+ the whole map, including historical data.
+
+
+ Users responsible for determining what issues
+ should be current at any time. Able to set current issues; able
+ to add issues.
+
+
+ Able to read and update canvasser records, team
+ membership records, team organisership records, issue expertise
+ records; able to add and update reference data
+ generally.
+
+
+
+ All electors known to the system; electors are
+ people believed to be entitled to vote in the current
+ campaign.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The signature of this elector, captured as SVG text,
+ as evidence they have consented to us holding data on them.
+ Null if they have not.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All genders which may be assigned to
+ electors.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All dwellings within addresses in the system; a
+ dwelling is a house, flat or appartment in which electors live.
+ Every address should have at least one dwelling; essentially,
+ an address maps onto a street door and dwellings map onto
+ what's behind that door. So a tenement or a block of flats
+ would be one address with many dwellings.
+
+
+
+
+
+
+
+
+
+
+ The part of the address which identifies the flat or
+ apartment within the building, if in a multiple occupancy
+ building.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Addresses of all buildings which contain
+ dwellings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Locality indexing; see issue #44. Note that
+ this property should be generated automatically from the
+ latitude and longitude: (+ (* 10000 ;; left-shift the
+ latitude component four digits (integer (* latitude 1000)))
+ (- ;; invert the sign of the longitude component, since ;;
+ we're interested in localities West of Greenwich. (integer (*
+ longitude 1000)))) We'll use a trigger to insert this. I
+ don't think it will ever appear in the user interface; it's
+ an implementation detail, not of interest to
+ users.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All visits made by canvassers to dwellings in
+ which opinions were recorded.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only in their immediate
+ area.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorities which may authenticate canvassers to
+ the system.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Issues believed to be of interest to electors,
+ about which they may have questions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intentions of electors to vote for options
+ elicited in visits.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The locality at which the intention was
+ recorded; used where an elector does not consent to have
+ polling intention stored against them. This is a locality as
+ described in
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The locality at which the intention was
+ recorded; used where an elector does not consent to have
+ polling intention stored against them. This is a locality
+ as described in
+
+
+
+
+
+
+ Primary users of the system: those actually
+ interviewing electors.
+
+
+
+
+
+
+
+
+
+
+
+
+ An image of the canvasser, so that other members of their
+ team can recognise them.
+
+
+
+ Information the canvasser supplies about themselves; an introduction.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Only relevant to issue experts.
+
+
+
+ But only their own record
+
+
+ But only canvassers in their own
+ team.
+
+
+ All canvassers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But should only be able to edit their own
+ record.
+
+
+
+
+
+
+
+
+
+ Requests for a followup with an issue
+ expert
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A role (essentially, the same as a group, but
+ application layer rather than database layer) of which a user
+ may be a member.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only their own group(s)
+
+
+
+
+
+ All groups
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ An event to which a team or teams are invited. Typically created by the team organiser(s).
+ May be a training event, a social event or a canvassing session.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Electoral districts: TODO: Shape (polygon)
+ information will need to be added, for use in
+ maps.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions taken on followup
+ requests.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only for electors in their immediate
+ vicinity
+
+
+
+
+
+
+
+
+ Options in the election or referendum being
+ canvassed on
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Methods which may be used to follow up a followup request. Reference data.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/youyesyet.canonical.adl.xml b/youyesyet.canonical.adl.xml
new file mode 100644
index 0000000..26f3e9f
--- /dev/null
+++ b/youyesyet.canonical.adl.xml
@@ -0,0 +1,1054 @@
+
+
+
+
+ A web-app intended to be used by canvassers
+ campaigning for a 'Yes' vote in the second independence
+ referendum. The web-app will be delivered to canvassers out
+ knocking doors primarily through an HTML5/React single-page app
+ designed to work on a mobile phone; it's possible that someone
+ else may do an Android of iPhone native app to address the same
+ back end but at present I have no plans for this. There must also
+ be an administrative interface through which privileged users can
+ set the system up and authorise canvassers, and a 'followup'
+ interface through which issue-expert specialist canvassers can
+ address particular electors' queries.
+
+ See
+ https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/488478/Bulk_Data_Transfer_-_additional_validation_valid_from_12_November_2015.pdf,
+ section 3
+ A valid postcode.
+
+
+ All users
+
+
+ All users of the canvasser app Able to read and
+ add canvassing data in a limited radius around their current
+ position.
+
+
+ Organisers of canvassing teams Able to see and
+ modify data on the canvassers in the team(s) they organise;
+ able to add canvassers to their team; able to update canvassers
+ in their team, including resetting passwords and locking
+ accounts; able to see canvass data over the whole area in which
+ their team operates.
+
+
+ People expert on particular issues. Able to read
+ followup requests, and the electors to which they relate; able
+ to access (read/write) the issues wiki; able to write followuop
+ action records.
+
+
+ Users entitled to see an overview of the
+ canvassing data collected. Able to read canvassing data over
+ the whole map, including historical data.
+
+
+ Users responsible for determining what issues
+ should be current at any time. Able to set current issues; able
+ to add issues.
+
+
+ Able to read and update canvasser records, team
+ membership records, team organisership records, issue expertise
+ records; able to add and update reference data
+ generally.
+
+
+
+ All electors known to the system; electors are
+ people believed to be entitled to vote in the current
+ campaign.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The signature of this elector, captured as SVG text,
+ as evidence they have consented to us holding data on them.
+ Null if they have not.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All genders which may be assigned to
+ electors.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All dwellings within addresses in the system; a
+ dwelling is a house, flat or appartment in which electors live.
+ Every address should have at least one dwelling; essentially,
+ an address maps onto a street door and dwellings map onto
+ what's behind that door. So a tenement or a block of flats
+ would be one address with many dwellings.
+
+
+
+
+
+
+
+
+
+
+ The part of the address which identifies the flat or
+ apartment within the building, if in a multiple occupancy
+ building.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Addresses of all buildings which contain
+ dwellings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Locality indexing; see issue #44. Note that
+ this property should be generated automatically from the
+ latitude and longitude: (+ (* 10000 ;; left-shift the
+ latitude component four digits (integer (* latitude 1000)))
+ (- ;; invert the sign of the longitude component, since ;;
+ we're interested in localities West of Greenwich. (integer (*
+ longitude 1000)))) We'll use a trigger to insert this. I
+ don't think it will ever appear in the user interface; it's
+ an implementation detail, not of interest to
+ users.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All visits made by canvassers to dwellings in
+ which opinions were recorded.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only in their immediate
+ area.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorities which may authenticate canvassers to
+ the system.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Issues believed to be of interest to electors,
+ about which they may have questions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intentions of electors to vote for options
+ elicited in visits.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The locality at which the intention was
+ recorded; used where an elector does not consent to have
+ polling intention stored against them. This is a locality as
+ described in
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The locality at which the intention was
+ recorded; used where an elector does not consent to have
+ polling intention stored against them. This is a locality
+ as described in
+
+
+
+
+
+
+ Primary users of the system: those actually
+ interviewing electors.
+
+
+
+
+
+
+
+
+
+
+
+
+ An image of the canvasser, so that other members of their
+ team can recognise them.
+
+
+
+ Information the canvasser supplies about themselves; an introduction.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Only relevant to issue experts.
+
+
+
+ But only their own record
+
+
+ But only canvassers in their own
+ team.
+
+
+ All canvassers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But should only be able to edit their own
+ record.
+
+
+
+
+
+
+
+
+
+ Requests for a followup with an issue
+ expert
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A role (essentially, the same as a group, but
+ application layer rather than database layer) of which a user
+ may be a member.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only their own group(s)
+
+
+
+
+
+ All groups
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ An event to which a team or teams are invited. Typically created by the team organiser(s).
+ May be a training event, a social event or a canvassing session.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Electoral districts: TODO: Shape (polygon)
+ information will need to be added, for use in
+ maps.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions taken on followup
+ requests.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ But only for electors in their immediate
+ vicinity
+
+
+
+
+
+
+
+
+ Options in the election or referendum being
+ canvassed on
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Methods which may be used to follow up a followup request. Reference data.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+