Merge branch 'release/0.2.0'

This commit is contained in:
simon 2017-07-21 17:47:31 +01:00
commit 72184d2c3c
81 changed files with 2327 additions and 610 deletions

2
.gitignore vendored
View file

@ -20,3 +20,5 @@ pom.xml.asc
.DS_Store .DS_Store
*-init.clj *-init.clj
profiles\.clj profiles\.clj
.bowerrc
bower.json

View file

@ -10,16 +10,12 @@ generated using Luminus version "2.9.11.05"
## Status ## Status
**Nothing works yet**. This is very early development code, a long way pre-alpha. Very early pre-alpha; user interface mostly works (enough to demonstrate), back end is hardly started.
## What is it supposed to do? ## What is it supposed to do?
To understand what I'm aiming for, read [this essay](http://blog.journeyman.cc/2016/10/preparing-for-next-independence.html). Design documentation, such as there is of it yet, is in the *dummy* sub-directory. Also look at [src/clj/youyesyet/db/schema.clj](https://github.com/simon-brooke/youyesyet/blob/master/src/clj/youyesyet/db/schema.clj). To understand what I'm aiming for, read [this essay](http://blog.journeyman.cc/2016/10/preparing-for-next-independence.html). Design documentation, such as there is of it yet, is in the *dummy* sub-directory. Also look at [src/clj/youyesyet/db/schema.clj](https://github.com/simon-brooke/youyesyet/blob/master/src/clj/youyesyet/db/schema.clj).
### Dummy website
I've put a [dummy site](http://www.journeyman.cc/~simon/tmp/yyy-dummy/index.html) up for people to play with. Obviously it's intended to work with mobile phones.
## Prerequisites ## Prerequisites
You will need [Leiningen][1] 2.0 or above installed. The database required must be [Postgres][2] 9.3 or above. You will need [Leiningen][1] 2.0 or above installed. The database required must be [Postgres][2] 9.3 or above.
@ -49,6 +45,16 @@ You should also read the [User-Oriented Specification](doc/specification/userspe
## Getting the database up ## Getting the database up
You'll need a file *profiles.clj*, with content similar to the following; it's not in the repository because it contains passwords.
;; WARNING
;; The profiles.clj file is used for local environment variables, such as database credentials.
;; This file is listed in .gitignore and will be excluded from version control by Git.
{:profiles/dev {:env {:database-url "jdbc:postgresql://127.0.0.1/youyesyet_dev?user=youyesyet&password=yourverysecurepassword"}}
:profiles/test {:env {:database-url "jdbc:postgresql://127.0.0.1/youyesyet_test?user=youyesyet&password=yourverysecurepassword"}}}
Do get the database initialised, run Do get the database initialised, run
createdb youyesyet_dev createdb youyesyet_dev
@ -59,9 +65,11 @@ followed by
**NOTE THAT** in the namespace *youyesyet.db.schema*, there are a series of functions *create-xxx-table!*. These are a snare and a delusion; they are how I originally bootstrapped the database, but are no longer used (and should probably be deleted). The database is now constructed using [migratus](https://github.com/yogthos/migratus). **NOTE THAT** in the namespace *youyesyet.db.schema*, there are a series of functions *create-xxx-table!*. These are a snare and a delusion; they are how I originally bootstrapped the database, but are no longer used (and should probably be deleted). The database is now constructed using [migratus](https://github.com/yogthos/migratus).
## Running ## Running in a dev environment
To start a web server for the application, run: To run in a dev environment, checkout the *develop* branch
To start a development web server for the application, run:
lein run lein run
@ -73,6 +81,10 @@ as above; in the other, run
lein figwheel lein figwheel
## Running in a production environment
Doesn't really work yet; if you want to try it, see [Bug #36](https://github.com/simon-brooke/youyesyet/issues/36) and check out the associated feature branch.
## Working on this project ## Working on this project
You should get the [ZenHub](https://github.com/integrations/zenhub) plug-in for your favourite browser; I use ZenHub for project management, and you (and I) will find collaborating much easier if you do. To join in You should get the [ZenHub](https://github.com/integrations/zenhub) plug-in for your favourite browser; I use ZenHub for project management, and you (and I) will find collaborating much easier if you do. To join in

View file

@ -0,0 +1,235 @@
# Database Specification
Note that this is a work in progress. Read it in concert with the Entity-Relationship Diagram.
Tables are listed in alphabetical order.
## Address
The postal address of a building which contains at least one dwelling at which electors are registered.
CREATE TABLE IF NOT EXISTS addresses (
id integer NOT NULL,
address character varying(256) NOT NULL,
postcode character varying(16),
phone character varying(16),
district_id integer,
latitude real,
longitude real
);
## Authority
An *oauth* authority which authenticates canvassers. *Note that* there will need to be substantially more in this table but I don't yet know what.
CREATE TABLE IF NOT EXISTS authorities (
id character varying(32) NOT NULL
);
## Canvasser
A user of the system.
CREATE TABLE IF NOT EXISTS canvassers (
id serial,
username character varying(32) NOT NULL,
fullname character varying(64) NOT NULL,
elector_id integer,
address_id integer NOT NULL,
phone character varying(16),
email character varying(128),
authority_id character varying(32) NOT NULL,
authorised boolean
);
## District
An electoral district.
CREATE TABLE IF NOT EXISTS districts (
id integer NOT NULL,
name character varying(64) NOT NULL
);
## Dwelling
A dwelling at which electors are registered. Most addresses obviously have only one dwelling, but in flatted buildings there will be multiple dwellings. The **sub\_address** field contains
information to distinguish the dwelling, e.g. 'flat 2.1'.
CREATE TABLE IF NOT EXISTS dwellings (
id serial NOT NULL primary key,
address_id integer NOT NULL references addresses(id),
sub_address varchar(16)
);
## Elector
Someone entitled to cast a vote in the referendum.
CREATE TABLE IF NOT EXISTS electors (
id integer NOT NULL,
name character varying(64) NOT NULL,
dwelling_id integer NOT NULL,
phone character varying(16),
email character varying(128)
);
## Followup Action
An action performed by an issue expert in response to a followup request.
CREATE TABLE IF NOT EXISTS followupactions (
id integer NOT NULL,
request_id integer NOT NULL,
actor integer NOT NULL,
date timestamp with time zone DEFAULT now() NOT NULL,
notes text,
closed boolean
);
## Followup Method
A method for responding to a followup request; reference data.
CREATE TABLE IF NOT EXISTS followupmethods (
id character varying(32) NOT NULL
);
insert into followupmethods values ('Telephone');
insert into followupmethods values ('eMail');
insert into followupmethods values ('Post');
## Followup Request
A request recorded by a canvasser for an issue expert to contact an elector with regard to a particular issue.
CREATE TABLE IF NOT EXISTS followuprequests (
id integer NOT NULL,
elector_id integer NOT NULL,
visit_id integer NOT NULL,
issue_id character varying(32) NOT NULL,
method_id character varying(32) NOT NULL
);
## Intention
An intention, by an elector, to vote for an option; captured by a canvasser during a visit.
CREATE TABLE IF NOT EXISTS intentions (
id serial not null,
elector integer not null references elector(id),
option varchar(32) not null references option(id),
visit integer not null references visit(id),
date timestamp with time zone DEFAULT now() NOT NULL
);
## Issue
An issue which might affect electors' decisions regarding their intention.
CREATE TABLE IF NOT EXISTS issues (
id character varying(32) NOT NULL,
url character varying(256),
content varchar(1024),
current default false
);
## Issue expertise
Expertise of a canvasser able to use a method, in an issue.
CREATE TABLE IF NOT EXISTS issueexpertise (
canvasser_id integer NOT NULL,
issue_id character varying(32) NOT NULL,
method_id character varying(32) NOT NULL
);
## Option
An option for which an elector may have an intention to vote.
CREATE TABLE IF NOT EXISTS options (
id character varying(32) NOT NULL
);
## Role
A role (other than basic *Canvasser*) that a user may have in the system. Reference data.
create table if not exists roles (
id serial primary key,
name varchar(64) not null
);
## Role Member
Membership of a user (*Canvasser*) of an additional role; link table.
create table if not exists rolememberships (
role_id integer not null references roles(id),
canvasser_id integer not null references canvassers(id)
);
## Team
A team of canvassers in a locality who are known to one another and frequently
canvas together.
create table if not exists teams (
id serial primary key,
name varchar(64) not null,
district_id integer not null references districts(id),
latitude real,
longitude real
);
## Team Member
Membership of a user (*Canvasser*) of a particular team. Canvassers may join multiple teams. Link table.
create table if not exists teammemberships (
team_id integer not null references teams(id),
canvasser_id integer not null references canvassers(id)
);
## Team Organiser
A relationship which defines a user (*Canvasser*) as an organiser of a team. A team may
have more than one organiser. An organiser (if they also have the role 'Recruiter', which
they often will have) may recruit additional Canvassers as members of their team, or
accept applications by canvassers to join their team. An organiser may promote a member of
the team to organiser of the team, and may also exclude a member from the team.
create table if not exists teamorganiserships (
team_id integer not null references teams(id),
canvasser_id integer not null references canvassers(id)
);
## Visit
A visit by a canvasser to an address on a date to solicit intentions from electors.
CREATE TABLE IF NOT EXISTS visits (
id integer NOT NULL,
address_id integer NOT NULL,
canvasser_id integer NOT NULL,
date timestamp with time zone DEFAULT now() NOT NULL
);

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -22,7 +22,7 @@ Database reads are probably more infrequent. Each client will obviously need to
Mobile phones typically can have intermittent network access. The client must be able to buffer a queue of records to be stored, and must not prevent the user from moving on to the next doorstep just because the data from the last visit has not yet been stored. There should probably be some on-screen indication of when there is unsent buffered data. Mobile phones typically can have intermittent network access. The client must be able to buffer a queue of records to be stored, and must not prevent the user from moving on to the next doorstep just because the data from the last visit has not yet been stored. There should probably be some on-screen indication of when there is unsent buffered data.
### Pattern of canvassing ### Pattern of canvassing
Canvassing takes place typically between 6:30pm and 9:00pm on a weekday evening. There will be some canvassing outside this period, but not enough to create significant load. Canvassing will be higher on dry nights than on wet ones, and will probably ramp up through the campaign. Canvassing takes place typically between 6:30pm and 9:00pm on a weekday evening. There will be some canvassing outside this period, but not enough to create significant load. Canvassing will be higher on dry nights than on wet ones, and will probably ramp up through the campaign.
@ -40,7 +40,7 @@ This means that the maximum number of transactions per second across Scotland is
700 transactions per second is not a very large number. We should be able to support this level of load on a single server. But what if we can't? 700 transactions per second is not a very large number. We should be able to support this level of load on a single server. But what if we can't?
## Spreading the load ## Spreading the load
### Caching and memoizing ### Caching and memoizing

View file

@ -1,4 +1,4 @@
### YouYesYet: User-oriented specification # YouYesYet: User-oriented specification
## Overview ## Overview
@ -75,18 +75,26 @@ Note that:
The map view shows a map of the streets immediately around their current location, overlaid, on dwellings where canvas has already been done, with icons indicating the voting preference expressed, and with the dwellings where canvassing is still required marked with an icon indicating this: The map view shows a map of the streets immediately around their current location, overlaid, on dwellings where canvas has already been done, with icons indicating the voting preference expressed, and with the dwellings where canvassing is still required marked with an icon indicating this:
![Map View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/mapview_800.png) ![Map View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/mapview.png)
Selecting a building on the map leads to Selecting a building on the map leads to
1. On buildings with multiple flats, a schematic view of the flats in the building (I haven't yet really got a good idea how to do this; it may be just a list). Selecting a flat from this view leads to the *Electors View*; 1. On buildings with multiple flats, the *Building View*;
2. On buildings with only one dwelling, the *Electors View*. 2. On buildings with only one dwelling, the *Electors View*.
## Building View
A list of dwellings in a building.
![Building View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/building.png)
Selecting a flat from this view leads to the *Electors View*.
## Electors View ## Electors View
The *Electors View* shows a schematic of the registered electors in a dwelling: The *Electors View* shows a schematic of the registered electors in a dwelling:
![Electors View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/occupants_800.png) ![Electors View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/occupants.png)
One figure is shown for each elector, labelled with their name. In the dummy pages I've shown gendered stick figures, because I believe that in many casesthis will help the canvasser identify the person who has answered the door; but this may be seen as excluding electors with non-binary gender, and, in any case, I believe we don't actually get gender data (other than salutation) in the electoral roll data. So this may have to be reconsidered. One figure is shown for each elector, labelled with their name. In the dummy pages I've shown gendered stick figures, because I believe that in many casesthis will help the canvasser identify the person who has answered the door; but this may be seen as excluding electors with non-binary gender, and, in any case, I believe we don't actually get gender data (other than salutation) in the electoral roll data. So this may have to be reconsidered.
@ -112,9 +120,18 @@ The *Issues View* is a simple list of issues:
| Other | | Other |
+------------------------------------------------+ +------------------------------------------------+
![Issues View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/issues.png)
This list will not be hard-coded but will be dynamic; thus, if we find an issue we didn't predict is regularly coming up on the doorstep an *Administrator* can add it to the list. This list will not be hard-coded but will be dynamic; thus, if we find an issue we didn't predict is regularly coming up on the doorstep an *Administrator* can add it to the list.
Selecting the back button from the *Issues View* returns to the *Electors View*. Selecting any option from the Issues view leads to a single page giving top level points the canvasser can make to the elector on the doorstep, and a link to a *Followup Request* form. There is also a 'back' button allowing the user to return to the *Issues View* Selecting the back button from the *Issues View* returns to the *Electors View*. Selecting any option from the Issues view leads to the *Issue View*.
## Issue View
A single page giving top level points the canvasser can make to the elector on the doorstep, regarding the selected issue; and a link to a *Followup Request* form. There is also a 'back' button allowing the user to return to the *Issues View*.
![Issue View](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/issue.png)
## Followup Request form ## Followup Request form
@ -132,6 +149,8 @@ The *Followup Request* form is a simple form which allows the canvasser to recor
| | | |
+------------------------------------------------+ +------------------------------------------------+
![Followup Request Form](https://raw.githubusercontent.com/simon-brooke/youyesyet/master/dummies/followup.png)
## How Street Canvassers will use the system ## How Street Canvassers will use the system
Street Canvassers will typically use the system by Street Canvassers will typically use the system by

BIN
dummies/building.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
dummies/building.xcf Normal file

Binary file not shown.

BIN
dummies/followup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
dummies/followup.xcf Normal file

Binary file not shown.

BIN
dummies/issue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
dummies/issue.xcf Normal file

Binary file not shown.

BIN
dummies/issues.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
dummies/issues.xcf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

View file

@ -1,5 +1,5 @@
(ns ^:figwheel-no-load youyesyet.app (ns ^:figwheel-no-load youyesyet.canvasser-app.app
(:require [youyesyet.core :as core] (:require [youyesyet.canvasser-app.core :as core]
[devtools.core :as devtools] [devtools.core :as devtools]
[figwheel.client :as figwheel :include-macros true])) [figwheel.client :as figwheel :include-macros true]))

View file

@ -1,5 +1,5 @@
(ns youyesyet.app (ns youyesyet.app
(:require [youyesyet.core :as core])) (:require [youyesyet.canvasser-app.core :as core]))
;;ignore println statements in prod ;;ignore println statements in prod
(set! *print-fn* (fn [& _])) (set! *print-fn* (fn [& _]))

View file

@ -1,9 +1,11 @@
/* I'm not certain that this file is still needed at all; however, it doesn't seem to be doing any harm. */
/* Things which should not get renamed when compiling ClojureScript */ /* Things which should not get renamed when compiling ClojureScript */
/* this block relates to the use of Leaflet */ /* this block relates to the use of Leaflet */
var L = { var L = {
"map": { "map": {
"setView": function(){} "setView": function(){},
"eg": function(){}
}, },
"tileLayer": { "tileLayer": {
"addTo": function(){} "addTo": function(){}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,54 +1,52 @@
(defproject youyesyet "0.1.0-SNAPSHOT" (defproject youyesyet "0.2.0"
:description "Canvassing tool for referenda" :description "Canvassing tool for referenda"
:url "https://github.com/simon-brooke/youyesyet" :url "https://github.com/simon-brooke/youyesyet"
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[bouncer "1.0.1"]
[org.clojure/clojurescript "1.9.229" :scope "provided"]
[ring/ring-servlet "1.5.1"]
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
[clj-oauth "1.5.5"]
[ch.qos.logback/logback-classic "1.2.2"] [ch.qos.logback/logback-classic "1.2.2"]
[re-frame "0.9.2"] [clj-oauth "1.5.5"]
[cljsjs/react-leaflet "0.12.3-4"]
[cljs-ajax "0.5.8"] [cljs-ajax "0.5.8"]
[secretary "1.2.3"] [compojure "1.5.2"]
[reagent-utils "0.2.1"] [conman "0.6.3"]
[reagent "0.6.1"] [cprop "0.1.10"]
[korma "0.4.3"] [korma "0.4.3"]
[selmer "1.10.6"] [lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
[luminus/ring-ttl-session "0.3.1"]
[luminus-nrepl "0.1.4"]
[luminus-migrations "0.3.0"]
[markdown-clj "0.9.98"] [markdown-clj "0.9.98"]
[ring-middleware-format "0.7.2"] [metosin/compojure-api "1.1.10"]
[metosin/ring-http-response "0.8.2"] [metosin/ring-http-response "0.8.2"]
[bouncer "1.0.1"] [migratus "0.8.33"]
[mount "0.1.11"]
[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.229" :scope "provided"]
[org.clojure/tools.cli "0.3.5"]
[org.clojure/tools.logging "0.3.1"]
[org.postgresql/postgresql "9.4.1212"]
[org.webjars/bootstrap "4.0.0-alpha.6-1"] [org.webjars/bootstrap "4.0.0-alpha.6-1"]
[org.webjars/font-awesome "4.7.0"] [org.webjars/font-awesome "4.7.0"]
[org.webjars.bower/tether "1.4.0"] [org.webjars.bower/tether "1.4.0"]
[org.clojure/tools.logging "0.3.1"] [re-frame "0.9.2"]
[compojure "1.5.2"] [reagent "0.6.1"]
[metosin/compojure-api "1.1.10"] [reagent-utils "0.2.1"]
[ring-webjars "0.1.1"] [ring-middleware-format "0.7.2"]
[ring/ring-defaults "0.2.3"] [ring/ring-defaults "0.2.3"]
[luminus/ring-ttl-session "0.3.1"] [ring/ring-servlet "1.5.1"]
[mount "0.1.11"] [ring-webjars "0.1.1"]
[cprop "0.1.10"] [secretary "1.2.3"]
[org.clojure/tools.cli "0.3.5"] [selmer "1.10.6"]]
[migratus "0.8.33"]
[luminus-nrepl "0.1.4"]
[luminus-migrations "0.3.0"]
[conman "0.6.3"]
[org.postgresql/postgresql "9.4.1212"]
]
:min-lein-version "2.0.0" :min-lein-version "2.0.0"
:license {:name "GNU General Public License v2"
:url "http://www.gnu.org/licenses/gpl-2.0.html"}
:jvm-opts ["-server" "-Dconf=.lein-env"] :jvm-opts ["-server" "-Dconf=.lein-env"]
:source-paths ["src/clj" "src/cljc"] :source-paths ["src/clj" "src/cljc"]
:test-paths ["test/clj"]
:resource-paths ["resources" "target/cljsbuild"] :resource-paths ["resources" "target/cljsbuild"]
:target-path "target/%s/" :target-path "target/%s/"
:main youyesyet.core :main ^:skip-aot youyesyet.core
:migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")} :migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")}
:plugins [[lein-cprop "1.0.1"] :plugins [[lein-cprop "1.0.1"]
@ -57,17 +55,19 @@
[lein-cljsbuild "1.1.4"] [lein-cljsbuild "1.1.4"]
[lein-uberwar "0.2.0"] [lein-uberwar "0.2.0"]
[lein-bower "0.5.1"] [lein-bower "0.5.1"]
[lein-less "1.7.5"]] [lein-less "1.7.5"]
[lein-codox "0.10.3"]]
:bower-dependencies [[leaflet "0.7.3"]] :bower-dependencies [[leaflet "0.7.3"]]
:cucumber-feature-paths ["test/clj/features"] :cucumber-feature-paths ["test/clj/features"]
:hooks [leiningen.less] :codox {:metadata {:doc "FIXME: write docs"}
:languages [:clojure :clojurescript]
:source-paths ["src/clj" "src/cljc" "src/cljs"]}
:uberwar :uberwar
{:prep-tasks ["compile" "bower" ["cljsbuild" "once" "min"]] {:handler youyesyet.handler/app
:handler youyesyet.handler/app
:init youyesyet.handler/init :init youyesyet.handler/init
:destroy youyesyet.handler/destroy :destroy youyesyet.handler/destroy
:name "youyesyet.war"} :name "youyesyet.war"}
@ -81,74 +81,68 @@
:css-dirs ["resources/public/css"] :css-dirs ["resources/public/css"]
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
:externs ["externs.js"]
:profiles :profiles
{:uberjar {:omit-source true {:uberjar {:omit-source true
:prep-tasks ["compile" ["bower" "install"] ["cljsbuild" "once" "min"]] :prep-tasks ["compile" ["cljsbuild" "once" "min"]]
:cljsbuild :cljsbuild
{:builds {:builds
{:min {:min
{:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"] {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"]
:compiler :compiler
{:optimizations :advanced {:output-to "target/cljsbuild/public/js/app.js"
:pretty-print false}}}} :optimizations :advanced
:pretty-print false
:closure-warnings
{:externs-validation :off :non-standard-jsdoc :off}
:externs ["react/externs/react.js"]}}}}
:aot :all :aot :all
:uberjar-name "youyesyet.jar" :uberjar-name "youyesyet.jar"
:source-paths ["env/prod/clj"] :source-paths ["env/prod/clj"]
:resource-paths ["env/prod/resources"]} :resource-paths ["env/prod/resources"]}
:dev [:project/dev :profiles/dev] :dev [:project/dev :profiles/dev]
:test [:project/dev :project/test :profiles/test] :test [:project/dev :project/test :profiles/test]
:project/dev {:dependencies [[prone "1.1.4"] :project/dev {:dependencies [[prone "1.1.4"]
[ring/ring-mock "0.3.0"] [ring/ring-mock "0.3.0"]
[ring/ring-devel "1.5.1"] [ring/ring-devel "1.5.1"]
[luminus-jetty "0.1.4"] [org.webjars/webjars-locator-jboss-vfs "0.1.0"]
[luminus-immutant "0.2.3"]
[pjstadig/humane-test-output "0.8.1"] [pjstadig/humane-test-output "0.8.1"]
[org.clojure/core.cache "0.6.5"]
[org.apache.httpcomponents/httpcore "4.4.6"]
[clj-webdriver/clj-webdriver "0.7.2"]
[org.seleniumhq.selenium/selenium-server "3.3.1" :exclusions [org.seleniumhq.selenium/selenium-support]]
[doo "0.1.7"]
[binaryage/devtools "0.9.2"] [binaryage/devtools "0.9.2"]
[figwheel-sidecar "0.5.9"]
[com.cemerick/piggieback "0.2.2-SNAPSHOT"] [com.cemerick/piggieback "0.2.2-SNAPSHOT"]
[directory-naming/naming-java "0.8"]] [directory-naming/naming-java "0.8"]
:plugins [[com.jakemccrary/lein-test-refresh "0.14.0"] [doo "0.1.7"]
[figwheel-sidecar "0.5.9"]]
:plugins [[com.jakemccrary/lein-test-refresh "0.18.1"]
[lein-doo "0.1.7"] [lein-doo "0.1.7"]
[lein-figwheel "0.5.9"] [lein-figwheel "0.5.9"]
[org.clojure/clojurescript "1.9.229"]] [org.clojure/clojurescript "1.9.495"]]
:cljsbuild :cljsbuild
{:builds {:builds
{:app {:app
{:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"]
:compiler :compiler
{:main "youyesyet.app" {:main "youyesyet.canvasser-app.app"
:asset-path "/js/out" :asset-path "/js/out"
:externs ["react/externs/react.js" "externs.js"]
:output-to "target/cljsbuild/public/js/app.js" :output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js/out" :output-dir "target/cljsbuild/public/js/out"
:source-map true :source-map true
:optimizations :none :optimizations :none
:pretty-print true}}}} :pretty-print true}}}}
:doo {:build "test"} :doo {:build "test"}
:source-paths ["env/dev/clj" "test/clj"] :source-paths ["env/dev/clj"]
:resource-paths ["env/dev/resources"] :resource-paths ["env/dev/resources"]
:repl-options {:init-ns user} :repl-options {:init-ns user}
:injections [(require 'pjstadig.humane-test-output) :injections [(require 'pjstadig.humane-test-output)
(pjstadig.humane-test-output/activate!)]} (pjstadig.humane-test-output/activate!)]}
:project/test {:resource-paths ["env/dev/resources" "env/test/resources"] :project/test {:resource-paths ["env/test/resources"]
:cljsbuild :cljsbuild
{:builds {:builds
{:test {:test
{:source-paths ["src/cljc" "src/cljs" "test/cljs"] {:source-paths ["src/cljc" "src/cljs" "test/cljs"]
:compiler :compiler
{:output-to "target/test.js" {:output-to "target/test.js"
:externs ["react/externs/react.js" "externs.js"]
:main "youyesyet.doo-runner" :main "youyesyet.doo-runner"
:optimizations :whitespace :optimizations :whitespace
:pretty-print true}}}} :pretty-print true}}}}

View file

@ -126,6 +126,7 @@ CREATE TABLE IF NOT EXISTS canvassers (
phone character varying(16), phone character varying(16),
email character varying(128), email character varying(128),
authority_id character varying(32) NOT NULL, authority_id character varying(32) NOT NULL,
introduced_by int references canvassers(id),
authorised boolean authorised boolean
); );
--;; --;;
@ -534,6 +535,8 @@ ALTER TABLE ONLY canvassers
ADD CONSTRAINT canvassers_elector_id_fkey FOREIGN KEY (elector_id) REFERENCES electors(id); ADD CONSTRAINT canvassers_elector_id_fkey FOREIGN KEY (elector_id) REFERENCES electors(id);
--;; --;;
create unique index canvassers_username_ix on canvassers (username);
create unique index canvassers_email_ix on canvassers(email);
-- --
-- Name: electors_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet -- Name: electors_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet

View file

@ -0,0 +1,17 @@
-- this is just a teardown of everything set up in the corresponding .up.sql file
delete from roles where name = 'Expert';
delete from roles where name = 'Administrator';
delete from roles where name = 'Recruiter';
delete from roles where name = 'Organiser';
delete from roles where name = 'Editor';
alter table issues drop column content;
alter table issues drop column current;
delete from issues where id = 'Currency';
delete from issues where id = 'Monarchy';
delete from issues where id = 'Defence';
delete from options where id = 'Yes';
delete from options where id = 'No';

View file

@ -0,0 +1,58 @@
-- We don't explicitly instantiate the 'Canvasser' role since every user is
-- deemed to be a canvasser.
-- an 'Expert' is someone with expertise in one or more issues, who is
-- trusted to discuss those issues in detail with electors.
insert into roles (name) values ('Expert');
-- an 'Administrator' is someone entitled to broadly alter reference data
-- throughout the system.
insert into roles (name) values ('Administrator');
-- a 'Recruiter' is someone entitled to invite other people to become users
-- ('Canvassers'). A Recruiter is entitled to lock the account of anyone they
-- have recruited, recursively.
insert into roles (name) values ('Recruiter');
-- an 'Organiser' is someone who organises one or more local teams. An Organiser
-- is entitled to exclude any Canvasser from any team they organise.
insert into roles (name) values ('Organiser');
-- an 'Editor' is someone entitled to add and edit issues.
insert into roles (name) values ('Editor');
-- issue text is local; there may still in addition be a further link to more
-- information, but the basic issue text should be part of the issue record.
-- The text should fit on a phone screen without scrolling, so is reasonably
-- short.
alter table issues add column content varchar(1024);
-- an issue may be current or not current; when not current it is not deleted
-- from the system but kept because it may become current again later. Only
-- current issues are shown in the app. Typically not fewer than three and not
-- more than about seven issues should be current at any time.
alter table issues add column current boolean default false;
insert into issues (id, content, current) values ('Currency',
'Scotland could keep the Pound, or use the Euro. But we could also set up a new currency of our own.',
true);
insert into issues (id, content, current) values ('Monarchy',
'Scotland could keep the Queen. This is an issue to be decided after independence.',
true);
insert into issues (id, content, current) values ('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.',
true);
insert into options (id) values ('Yes');
insert into options (id) values ('No');

View file

@ -0,0 +1,11 @@
drop view if exists roles_by_canvasser;
drop view if exists teams_by_canvasser;
drop view if exists canvassers_by_team;
drop view if exists canvassers_by_introducer;
drop view if exists teams_by_organiser;
drop view if exists organisers_by_team;

View file

@ -0,0 +1,59 @@
create view roles_by_canvasser as
select canvassers.id as canvasser, roles.name
from roles, rolememberships, canvassers
where roles.id = rolememberships.role_id
and canvassers.id = rolememberships.canvasser_id
and canvassers.authorised = true;
create view teams_by_canvasser as
select canvassers.id as canvasser, teams.id, teams.name, teams.latitude, teams.longitude
from teams, teammemberships, canvassers
where teams.id = teammemberships.team_id
and canvassers.id = teammemberships.canvasser_id;
create view canvassers_by_team as
select teams.id as team,
canvassers.id,
canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.phone
from teams, teammemberships, canvassers
where teams.id = teammemberships.team_id
and canvassers.id = teammemberships.canvasser_id
and canvassers.authorised = true;
create view canvassers_by_introducer as
select introducers.id as introducer,
canvassers.id as canvasser,
canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.phone,
canvassers.authorised
from canvassers, canvassers as introducers
where introducers.id = canvassers.introduced_by;
create view teams_by_organiser as
select canvassers.id as organiser,
teams.id,
teams.name,
teams.latitude,
teams.longitude
from teams, teamorganiserships, canvassers
where teams.id = teamorganiserships.team_id
and canvassers.id = teamorganiserships.canvasser_id
and canvassers.authorised = true;
create view organisers_by_team as
select teams.id as team,
canvassers.id,
canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.phone
from teams, teamorganiserships, canvassers
where teams.id = teamorganiserships.team_id
and canvassers.id = teamorganiserships.canvasser_id
and canvassers.authorised = true;

View file

@ -0,0 +1,69 @@
--------------------------------------------------------------------------------
----
---- 20170721084900.up.sql: add dwellings table, to deal with flatted addresses.
----
---- 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
----
--------------------------------------------------------------------------------
----
---- NOTE
---- This file is essentially a Postgres schema dump of a database schema which was
---- created with the function initdb! in the file src/clj/youyesyet/db/schema.clj.
---- This file has then been mildly massaged to work with Migratus.
---- Either this file or src/clj/youyesyet/db/schema.clj is redundant; schema.clj
---- represents the older, Korma, way of doing things but does not readily allow
---- for migrations; this file represents the newer Migratus/HugSQL way. I'm not
---- certain which of these paths I'm going to go down.
----
--------------------------------------------------------------------------------
alter table canvassers add column address_id integer references addresses(id);
--;;
alter table electors add column address_id integer references addresses(id);
--;;
alter table visits add column address_id integer references addresses(id);
--;;
update canvassers set address_id =
(select address_id from dwellings where id = canvassers.dwelling_id);
--;;
update electors set address_id =
(select address_id from dwellings where id = electors.dwelling_id);
--;;
update visits set address_id =
(select address_id from dwellings where id = visits.dwelling_id);
--;;
alter table canvassers alter column address_id set not null;
--;;
alter table electors alter column address_id set not null;
--;;
alter table visits alter column address_id set not null;
--;;
alter table canvassers drop column dwelling_id;
--;;
alter table electors drop column dwelling_id;
--;;
alter table visits drop column dwelling_id;
--;;
drop table if exists dwellings;
--;;

View file

@ -0,0 +1,87 @@
--------------------------------------------------------------------------------
----
---- 20170721084900.up.sql: add dwellings table, to deal with flatted addresses.
----
---- 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
----
--------------------------------------------------------------------------------
----
---- NOTE
---- This file is essentially a Postgres schema dump of a database schema which was
---- created with the function initdb! in the file src/clj/youyesyet/db/schema.clj.
---- This file has then been mildly massaged to work with Migratus.
---- Either this file or src/clj/youyesyet/db/schema.clj is redundant; schema.clj
---- represents the older, Korma, way of doing things but does not readily allow
---- for migrations; this file represents the newer Migratus/HugSQL way. I'm not
---- certain which of these paths I'm going to go down.
----
--------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dwellings (
id serial NOT NULL primary key,
address_id integer NOT NULL references addresses(id),
sub_address varchar(16)
);
--;;
ALTER TABLE public.dwellings OWNER TO youyesyet;
--;;
INSERT INTO dwellings (address_id, sub_address)
SELECT DISTINCT id, '' FROM addresses;
--;;
alter table canvassers add column dwelling_id integer references dwellings(id);
--;;
alter table electors add column dwelling_id integer references dwellings(id);
--;;
alter table visits add column dwelling_id integer references dwellings(id);
--;;
update canvassers set dwelling_id =
(select id from dwellings where address_id = canvassers.address_id);
--;;
update electors set dwelling_id =
(select id from dwellings where address_id = electors.address_id);
--;;
update visits set dwelling_id =
(select id from dwellings where address_id = visits.address_id);
--;;
alter table canvassers alter column dwelling_id set not null;
--;;
alter table electors alter column dwelling_id set not null;
--;;
alter table visits alter column dwelling_id set not null;
--;;
alter table canvassers drop constraint canvassers_address_id_fkey;
--;;
alter table electors drop constraint electors_address_id_fkey;
--;;
alter table visits drop constraint visits_address_id_fkey;
--;;
alter table canvassers drop column address_id;
--;;
alter table electors drop column address_id;
--;;
alter table visits drop column address_id;
--;;

View file

@ -62,11 +62,7 @@ footer {
padding: 0.25em 0; padding: 0.25em 0;
bottom: 0; bottom: 0;
position: fixed; position: fixed;
vertical-align: top;
z-index:150; z-index:150;
_position:absolute;
_top:expression(eval(document.documentElement.scrollTop+
(document.documentElement.clientHeight-this.offsetHeight)));
} }
footer div { footer div {
@ -365,8 +361,12 @@ th {
/* phones, and, indeed, smaller phones. Adapted to touch; display radically /* phones, and, indeed, smaller phones. Adapted to touch; display radically
* decluttered */ * decluttered */
@media all and (max-device-width: 768px) { @media all and (max-device-width: 768px) {
footer { button, input, select {
display: none; background-color: rgb( 50, 109, 177);
color: white;
font-size: 1.1em;
padding: 0.25em 1em;
border-radius: 0.5em;
} }
h1 { h1 {
@ -375,14 +375,6 @@ th {
padding-left: 75px; padding-left: 75px;
} }
input, select {
background-color: rgb( 50, 109, 177);
color: white;
font-size: 1.1em;
padding: 0.25em 1em;
border-radius: 0.5em;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -398,6 +390,10 @@ th {
padding-bottom: 2em; padding-bottom: 2em;
} }
#issue-text {
font-size: 200%;
}
#main-container { #main-container {
width: 100%; width: 100%;
margin: 0; margin: 0;

View file

@ -46,6 +46,7 @@
#nav-menu { #nav-menu {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
} }
#nav menu li { #nav menu li {
@ -126,6 +127,10 @@
/* phones, and, indeed, smaller phones. Adapted to touch; display radically /* phones, and, indeed, smaller phones. Adapted to touch; display radically
* decluttered */ * decluttered */
@media all and (max-device-width: 768px) { @media all and (max-device-width: 768px) {
footer {
display: none;
}
#nav{ #nav{
margin: 0; margin: 0;
padding: 0; padding: 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,21 +1,344 @@
-- :name create-user! :! :n ------------------------------------------------------------------------------;
-- :doc creates a new user record ----
INSERT INTO users ---- youyesyet.routes.authenticated: routes and pages for authenticated users.
(id, first_name, last_name, email, pass) ----
VALUES (:id, :first_name, :last_name, :email, :pass) ---- 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
----
------------------------------------------------------------------------------;
-- :name update-user! :! :n -- This file gets slurped in and converted into simple functions by the line
-- :doc update an existing user record -- in youyesyet.db.core.clj:
UPDATE users -- (conman/bind-connection *db* "sql/queries.sql")
SET first_name = :first_name, last_name = :last_name, email = :email -- the functions then appeare in the youyesyet.db.core namespace.
-- :name create-address! :! :n
-- :doc creates a new address record
INSERT INTO addresses
(address, postcode, district_id, latitude, longitude)
VALUES (:address, :postcode, :district, :latitude, :longitude)
RETURNING id
-- :name update-address! :! :n
-- :doc update an existing address record
UPDATE addresses
SET address = :address, postcode = :postcode, latitude = :latitude, longitude = :longitude
WHERE id = :id WHERE id = :id
-- :name get-user :? :1 -- :name get-address :? :1
-- :doc retrieve a user given the id. -- :doc retrieve a address given the id.
SELECT * FROM users SELECT * FROM addresses
WHERE id = :id WHERE id = :id
-- :name delete-user! :! :n -- :name get-addresses-by-postcode
-- :doc delete a user given the id
DELETE FROM users -- :name delete-address! :! :n
-- :doc delete a address given the id
DELETE FROM addresses
WHERE id = :id WHERE id = :id
-- :name create-authority! :! :n
-- :doc creates a new authority record
INSERT INTO authorities
(id)
VALUES (:id)
RETURNING id
-- :name update-authority! :! :n
-- :doc update an existing authority record
UPDATE authorities
SET id = :id
WHERE id = :id
-- :name get-authority :? :1
-- :doc retrieve a authority given the id.
SELECT * FROM authorities
WHERE id = :id
-- :name get-authorities :? :0
-- :doc retrieve all authorities
SELECT id FROM authorities
-- :name delete-authority! :! :n
-- :doc delete a authority given the id
DELETE FROM authorities
WHERE id = :id
-- :name create-canvasser! :! :n
-- :doc creates a new canvasser record
INSERT INTO canvassers
(username, fullname, elector_id, dwelling_id, phone, email, authority_id, authorised)
VALUES (:username, :fullname, :elector_id, :dwelling_id, :phone, :email, :authority_id, :authorised)
RETURNING id
-- :name update-canvasser! :! :n
-- :doc update an existing canvasser record
UPDATE canvassers
SET username = :username, fullname = :fullname, elector_id = :elector_id, dwelling_id = :dwelling_id, phone = :phone, email = :email, authority_id = :authority_id, authorised = :authorised
WHERE id = :id
-- :name get-canvasser :? :1
-- :doc retrieve a canvasser given the id.
SELECT * FROM canvassers
WHERE id = :id
-- :name get-canvasser-by-username :? :1
-- :doc rerieve a canvasser given the username.
SELECT * FROM canvassers
WHERE username = :username
-- :name get-canvasser-by-email :? :1
-- :doc rerieve a canvasser given the email address.
SELECT * FROM canvassers
WHERE email = :email
-- :name delete-canvasser! :! :n
-- :doc delete a canvasser given the id
DELETE FROM canvassers
WHERE id = :id
-- :name create-district! :! :n
-- :doc creates a new district record
INSERT INTO districts
(id, name)
VALUES (:id, :name)
RETURNING id
-- :name update-district! :! :n
-- :doc update an existing district record
UPDATE districts
SET name = :name
WHERE id = :id
-- :name get-district :? :1
-- :doc retrieve a district given the id.
SELECT * FROM districts
WHERE id = :id
-- :name delete-district! :! :n
-- :doc delete a district given the id
DELETE FROM districts
WHERE id = :id
-- :name get-dwelling :? :1
-- :doc retrieve a dwelling given the id.
SELECT * FROM dwellings
WHERE id = :id
-- :name delete-dwelling! :! :n
-- :doc delete a dwelling given the id
DELETE FROM dwellings
WHERE id = :id
-- :name create-dwelling! :! :n
-- :doc creates a new dwelling record
INSERT INTO dwellings
(id, address_id, sub_address)
VALUES (:id, :address_id, :sub_address)
RETURNING id
-- :name update-dwelling! :! :n
-- :doc update an existing dwelling record
UPDATE dwellings
SET address_id = :address_id,
sub_address = :sub_address
WHERE id = :id
-- :name get-dwelling :? :1
-- :doc retrieve a dwelling given the id.
SELECT * FROM dwellings
WHERE id = :id
-- :name delete-dwelling! :! :n
-- :doc delete a dwelling given the id
DELETE FROM dwellings
WHERE id = :id
-- :name create-elector! :! :n
-- :doc creates a new elector record
INSERT INTO electors
(name, dwelling_id, phone, email)
VALUES (:name, :dwelling_id, :phone, :email)
RETURNING id
-- :name update-elector! :! :n
-- :doc update an existing elector record
UPDATE electors
SET name = :name, dwelling_id = :dwelling_id, phone = :phone, email = :email
WHERE id = :id
-- :name get-elector :? :1
-- :doc retrieve a elector given the id.
SELECT * FROM electors
WHERE id = :id
-- :name delete-elector! :! :n
-- :doc delete a elector given the id
DELETE FROM electors
WHERE id = :id
-- :name create-followupaction! :! :n
-- :doc creates a new followupaction record
INSERT INTO followupactions
(request_id, actor, date, notes, closed)
VALUES (:request_id, :actor, :date, :notes, :closed)
RETURNING id
-- We don't update followup actions. They're permanent record.
-- :name get-followupaction :? :1
-- :doc retrieve a followupaction given the id.
SELECT * FROM followupactions
WHERE id = :id
-- We don't delete followup actions. They're permanent record.
-- followup methods are reference data, do not need to be programmatically maintained.
-- :name create-followuprequest! :! :n
-- :doc creates a new followupaction record
INSERT INTO followuprequests
(elector_id, visit_id, issue_id, method_id)
VALUES (:elector_id, :visit_id, :issue_id, :method_id)
RETURNING id
-- We don't update followup requests. They're permanent record.
-- :name get-followuprequest :? :1
-- :doc retrieve a followupaction given the id.
SELECT * FROM followuprequests
WHERE id = :id
-- We don't delete followup requests. They're permanent record.
-- :name create-issueexpertise! :! :n
-- :doc creates a new issueexpertise record
INSERT INTO issueexpertise
(canvasser_id, issue_id, method_id)
VALUES (:canvasser_id, :issue_id, :method_id)
-- issueexertise is a link table, doesn't have an id field.
-- :name update-issueexpertise! :! :n
-- :doc update an existing issueexpertise record
UPDATE issueexpertise
SET canvasser_id = :canvasser_id, issue_id = :issue_id, method_id = :method_id
WHERE id = :id
-- :name get-issueexpertise :? :1
-- :doc retrieve a issueexpertise given the canvasser_id -
-- getting it by its own id is unlikely to be interesting or useful.
SELECT * FROM issueexpertise
WHERE canvasser_id = :canvasser_id
-- :name delete-issueexpertise! :! :n
-- :doc delete a issueexpertise given the id
DELETE FROM issueexpertise
WHERE id = :id
-- :name create-issue! :! :n
-- :doc creates a new issue record
INSERT INTO issues
(id, url, content, current)
VALUES (:id, :url, :content, :current)
RETURNING id
-- :name update-issue! :! :n
-- :doc update an existing issue record
UPDATE issues
SET url = :url, content = :content, current = :current
WHERE id = :id
-- :name get-issue :? :1
-- :doc retrieve a issue given the id -
SELECT * FROM issues
WHERE id = :id
-- :name delete-issue! :! :n
-- :doc delete a issue given the id
DELETE FROM issues
WHERE id = :id
-- options is virtually reference data; it's not urgent to create a programmatic means of editing
-- :name create-visit! :! :n
-- :doc creates a new visit record
INSERT INTO visits
(dwelling_id, canvasser_id)
VALUES (:dwelling_id, :canvasser_id)
RETURNING id
-- visits is audit data; we don't update it.
-- :name get-visit :? :1
-- :doc retrieve a visit given the id.
SELECT * FROM visits
WHERE id = :id
-- visits is audit data; we don't delete it.
-- views are select only
-- :name get-roles-by-canvasser :? :*
-- :doc Get the role names for the canvasser with the specified id
select name from roles_by_canvasser
where canvasser = :canvasser
-- :name get-teams-by-canvasser :? :*
-- :doc Get details of the teams which the canvasser with the specified id is member of.
select * from teams_by_canvasser
where canvasser = :canvasser_id
-- :name get-canvassers-by-team :? :*
-- :doc Get details of all canvassers who are members of the team with the specified id
select * from canvassers_by_team
where team = :team_id
-- :name get-canvassers-by-team :? :*
-- :doc Get details of all authorised canvassers who are members of this team.
select * from canvassers_by_introducer
where introducer = :introducer_id
-- :name get-canvassers-by-search :? :*
-- :doc Get details of all authorised canvassers whose details match this search string.
select * from canvassers
where name like '%' || :search || '%'
or username like '%' || :search || '%'
or email like '%' || :search || '%'
-- :name get-teams_by_organiser :? :*
-- :doc Get details of all the teams organised by the canvasser with the specified id
select * from teams_by_organiser
where organiser = :organiser_id
-- :name get-organisers-by-team :? :*
-- :doc Get details of all organisers of the team with the specified id
select * from organisers_by_team
where team = :team_id

View file

@ -1,16 +1,5 @@
<!DOCTYPE html> {% extends "base-authenticated.html" %}
<html> {% block whole-page %}
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/yyy-common.css" />
<link rel="stylesheet" type="text/css" href="css/yyy-app.css" />
<link rel="stylesheet" type="text/css" href="css/spinner.css" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
<title>You Yes Yet?</title>
</head>
<body>
<div id="app"> <div id="app">
<div class="splash-screen"> <div class="splash-screen">
<div class="sk-fading-circle"> <div class="sk-fading-circle">
@ -33,21 +22,16 @@
You must enable JavaScript to use the You Yes Yet app. You must enable JavaScript to use the You Yes Yet app.
</p> </p>
</div> </div>
{% endblock %}
{% block extra-script %}
<!-- scripts and styles --> <!-- scripts and styles -->
<!-- ATTENTION \/ --> <!-- ATTENTION \/ -->
<!-- Leaflet --> <!-- Leaflet -->
<link rel="stylesheet" href="vendor/leaflet/dist/leaflet.css" /> <link rel="stylesheet" href="vendor/leaflet/dist/leaflet.css" />
<script src="vendor/leaflet/dist/leaflet.js"></script> <script src="vendor/leaflet/dist/leaflet.js"></script>
<!-- ATTENTION /\ --> <!-- ATTENTION /\ -->
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
</script>
{% script "/js/app.js" %} {% script "/js/app.js" %}
</body> {% endblock %}
</html>

View file

@ -3,12 +3,14 @@
<head> <head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/yyy-static.css" /> <link rel="stylesheet" type="text/css" href="css/yyy-common.css" />
<link rel="stylesheet" type="text/css" href="css/yyy-site.css" />
<link rel="stylesheet" type="text/css" href="css/spinner.css" /> <link rel="stylesheet" type="text/css" href="css/spinner.css" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
<title>{{title}}</title> <title>{{title}}</title>
</head> </head>
<body> <body>
{% block whole-page %}
<header> <header>
<div id="nav"> <div id="nav">
<img id="nav-icon" src="img/threelines.png" alt="Menu"/> <img id="nav-icon" src="img/threelines.png" alt="Menu"/>
@ -16,8 +18,12 @@
<li class=""><a href="index.html">Home</a></li> <li class=""><a href="index.html">Home</a></li>
<li class=""><a href="library.html">Library</a></li> <li class=""><a href="library.html">Library</a></li>
<li class=""><a href="register.html">Register</a></li> <li class=""><a href="register.html">Register</a></li>
<li class=""><a href="login.html">Login</a></li> <li class="">{% if user %}<a href="logout.html">Logout</a>
{% else %}<a href="login.html">Login</a>{% endif %}</li>
<li class=""><a href="about.html">About</a></li> <li class=""><a href="about.html">About</a></li>
{% if user %}
<li id="user"><a href="profile">Logged in as {{user.username}}</a></li>
{% endif %}
</menu> </menu>
</div> </div>
@ -28,12 +34,40 @@
<div id="main-container" class="container"> <div id="main-container" class="container">
<div id="big-links"> <div id="big-links">
{{big-links}} {% block big-links %}
{% endblock %}
</div> </div>
<div if="#content"> <div if="#content">
{{content}} {% block content %}
{% endblock %}
</div>
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div> </div>
</div> </div>
<footer>
<div id="credits">
<div>
<img src="img/credits/ric-logo.png" width="24" height="24"/>
A project of the
<a href="https://radical.scot/">Radical Independence Campaign</a> ||
Version {{version}}
</div>
<div>
<img height="16" width="16" alt="Clojure" src="img/credits/luminus-logo.png"/>Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> ||
<img height="16" width="16" alt="Clojure" src="img/credits/clojure-icon.gif"/> Powered by <a href="http://clojure.org">Clojure</a> ||
<img height="16" width="16" alt="GitHub" src="img/credits/github-logo-transparent.png"/>Find me/fork me on <a href="https://github.com/simon-brooke/smeagol">Github</a> ||
<img height="16" width="16" alt="Free Software Foundation" src="img/credits/gnu.small.png"/>Licensed under the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General Public License version 2.0</a>
</div>
</div>
</footer>
{% endblock %}
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
</script>
{% block extra-script %}
{% endblock %}
</body> </body>
</html> </html>

View file

@ -6,7 +6,7 @@
<link rel="stylesheet" type="text/css" href="css/yyy-common.css" /> <link rel="stylesheet" type="text/css" href="css/yyy-common.css" />
<link rel="stylesheet" type="text/css" href="css/yyy-site.css" /> <link rel="stylesheet" type="text/css" href="css/yyy-site.css" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
<title>{{title}}</title> <title>{% block title %}{% endblock %}{{title}}</title>
</head> </head>
<body> <body>
<header> <header>
@ -37,16 +37,16 @@
<footer> <footer>
<div id="credits"> <div id="credits">
<div> <div>
<img src="img/ric-logo.png" width="24" height="24"/> <img src="img/credits/ric-logo.png" width="24" height="24"/>
A project of the A project of the
<a href="https://radical.scot/">Radical Independence Campaign</a> || <a href="https://radical.scot/">Radical Independence Campaign</a> ||
Version {{version}} Version {{version}}
</div> </div>
<div> <div>
<img height="16" width="16" alt="Clojure" src="img/luminus-logo.png"/>Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> || <img height="16" width="16" alt="Clojure" src="img/credits/luminus-logo.png"/>Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> ||
<img height="16" width="16" alt="Clojure" src="img/clojure-icon.gif"/> Powered by <a href="http://clojure.org">Clojure</a> || <img height="16" width="16" alt="Clojure" src="img/credits/clojure-icon.gif"/> Powered by <a href="http://clojure.org">Clojure</a> ||
<img height="16" width="16" alt="GitHub" src="img/github-logo-transparent.png"/>Find me/fork me on <a href="https://github.com/simon-brooke/smeagol">Github</a> || <img height="16" width="16" alt="GitHub" src="img/credits/github-logo-transparent.png"/>Find me/fork me on <a href="https://github.com/simon-brooke/smeagol">Github</a> ||
<img height="16" width="16" alt="Free Software Foundation" src="img/gnu.small.png"/>Licensed under the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General Public License version 2.0</a> <img height="16" width="16" alt="Free Software Foundation" src="img/credits/gnu.small.png"/>Licensed under the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General Public License version 2.0</a>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -0,0 +1,60 @@
{% extends "base-authenticated.html" %}
{% block title %}
{% endblock %}
{% block content %}
<form action="edit-canvasser" method="post">
{% if canvasser %}
<input type="hidden" name="id" id="id" value="{{canvasser.id}}"/>
{% endif %}
<p class="widget">
<label for="fullname">Full name</label>
<input type="text" name="fullname" id="fullname" value="{{canvasser.fullname}}"/>
</p>
<p class="widget">
(TODO: Not absolutely sure what I'm going to do for an elector id widget yet.)
</p>
<p class="widget">
<label for="address">Address</label>
{% if address.id %}
<!-- if we already have an address, just show it with a link to edit it -->
<span class="pseudo-widget" id="address">
{{address.address}}
</span>
{% else %}
(TODO: Some sort of address lookup widget goes here.)
{% endif %}
</p>
<p class="widget">
<label for="phone">Phone number</label>
<input type="tel" name="phone" id="phone" value="{{canvasser.phone}}"/>
</p>
<p class="widget">
<label for="email">Email address</label>
<input type="email" name="email" id="email" value="{{canvasser.email}}"/>
</p>
<p class="widget">
<label for="authority_id">Authorised by</label>
<select name="authority_id" id="authority_id">
{% for authority in authorities %}
<option value="{{authority.id}}"
{% ifequal authority.id canvasser.authority_id %}selected {% endifequal %}>
{{authority.id}}
</option>
</select>
</p>
</p>
id serial,
username character varying(32) NOT NULL,
fullname character varying(64) NOT NULL,
elector_id integer,
address_id integer NOT NULL,
phone character varying(16),
email character varying(128),
authority_id character varying(32) NOT NULL,
introduced_by int references canvassers(id),
authorised boolean
</form>
{% endblock %}

View file

@ -1,9 +1,4 @@
{% extends "base-unauthenticated.html" %} {% extends "base-unauthenticated.html" %}
{% block big-links %}
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div>
{% endblock %}
{% block content %} {% block content %}
<p> <p>
We're not going to do login in the long term; we're going to use oauth. We're not going to do login in the long term; we're going to use oauth.

View file

@ -0,0 +1,16 @@
{% extends "base-authenticated.html" %}
{% block title %}
{{ user }}
{% endblock %}
{% block big-links %}
<div class="big-link-container">
<a href="app" class="big-link" id="big-link">Canvasser</a>
</div>
{% for role in roles %}
<div class="big-link-container">
<a href="{{role.name|lower}}" class="big-link" id="big-link">{{role.name}}</a>
</div>
{% endfor %}
{% endblock %}
{% block content %}
{% endblock %}

View file

@ -1,4 +1,6 @@
(ns youyesyet.config (ns ^{:doc "Read configuration."
:author "Simon Brooke"}
youyesyet.config
(:require [cprop.core :refer [load-config]] (:require [cprop.core :refer [load-config]]
[cprop.source :as source] [cprop.source :as source]
[mount.core :refer [args defstate]])) [mount.core :refer [args defstate]]))

View file

@ -1,4 +1,6 @@
(ns youyesyet.db.core (ns ^{:doc "Database access functions."
:author "Simon Brooke"}
youyesyet.db.core
(:require (:require
[cheshire.core :refer [generate-string parse-string]] [cheshire.core :refer [generate-string parse-string]]
[clojure.java.jdbc :as jdbc] [clojure.java.jdbc :as jdbc]
@ -15,12 +17,10 @@
Timestamp Timestamp
PreparedStatement])) PreparedStatement]))
(def ^:dynamic *db* {:name "java:comp/env/jdbc/EmployeeDB"}) (defstate ^:dynamic *db*
;; (defstate ^:dynamic *db* :start (conman/connect! {:jdbc-url (env :database-url)
;; :start (conman/connect! {:jdbc-url-env (env :database-url) :driver-class-name "org.postgresql.Driver"})
;; :jdbc-url "jdbc:postgresql://127.0.0.1/youyesyet_dev?user=youyesyet&password=thisisnotsecure" :stop (conman/disconnect! *db*))
;; :driver-class-name "org.postgresql.Driver"})
;; :stop (conman/disconnect! *db*))
(conman/bind-connection *db* "sql/queries.sql") (conman/bind-connection *db* "sql/queries.sql")

View file

@ -1,4 +1,5 @@
(ns youyesyet.db.schema (ns ^{:doc "Korma-flavour database setup, now obsolete but retained for documentation."
:author "Simon Brooke"} youyesyet.db.schema
(:require [clojure.java.jdbc :as sql] (:require [clojure.java.jdbc :as sql]
[korma.core :as kc] [korma.core :as kc]
[youyesyet.db.core :as yyydb])) [youyesyet.db.core :as yyydb]))

View file

@ -1,6 +1,9 @@
(ns youyesyet.handler (ns ^{:doc "Handlers for starting and stopping the webapp."
:author "Simon Brooke"}
youyesyet.handler
(:require [compojure.core :refer [routes wrap-routes]] (:require [compojure.core :refer [routes wrap-routes]]
[youyesyet.layout :refer [error-page]] [youyesyet.layout :refer [error-page]]
[youyesyet.routes.authenticated :refer [authenticated-routes]]
[youyesyet.routes.home :refer [home-routes]] [youyesyet.routes.home :refer [home-routes]]
[youyesyet.routes.oauth :refer [oauth-routes]] [youyesyet.routes.oauth :refer [oauth-routes]]
[compojure.route :as route] [compojure.route :as route]
@ -10,6 +13,29 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[youyesyet.config :refer [env]])) [youyesyet.config :refer [env]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.handler: handlers for starting and stopping the webapp.
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mount/defstate init-app (mount/defstate init-app
:start ((or (:init defaults) identity)) :start ((or (:init defaults) identity))
:stop ((or (:stop defaults) identity))) :stop ((or (:stop defaults) identity)))
@ -38,6 +64,7 @@
(wrap-routes middleware/wrap-csrf) (wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats)) (wrap-routes middleware/wrap-formats))
#'oauth-routes #'oauth-routes
#'authenticated-routes
(route/not-found (route/not-found
(:body (:body
(error-page {:status 404 (error-page {:status 404

View file

@ -1,4 +1,6 @@
(ns youyesyet.layout (ns^{:doc "Render web pages using Selmer tamplating markup."
:author "Simon Brooke"}
youyesyet.layout
(:require [selmer.parser :as parser] (:require [selmer.parser :as parser]
[selmer.filters :as filters] [selmer.filters :as filters]
[markdown.core :refer [md-to-html-string]] [markdown.core :refer [md-to-html-string]]
@ -12,6 +14,7 @@
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field))) (parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)])) (filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
(defn render (defn render
"renders the HTML template located relative to resources/templates" "renders the HTML template located relative to resources/templates"
[template & [params]] [template & [params]]
@ -26,6 +29,7 @@
:version (System/getProperty "youyesyet.version")))) :version (System/getProperty "youyesyet.version"))))
"text/html; charset=utf-8")) "text/html; charset=utf-8"))
(defn error-page (defn error-page
"error-details should be a map containing the following keys: "error-details should be a map containing the following keys:
:status - error status :status - error status

View file

@ -1,4 +1,6 @@
(ns youyesyet.middleware (ns ^{:doc "Plumbing, mainly boilerplate from Luminus."
:author "Simon Brooke"}
youyesyet.middleware
(:require [youyesyet.env :refer [defaults]] (:require [youyesyet.env :refer [defaults]]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[youyesyet.layout :refer [*app-context* error-page]] [youyesyet.layout :refer [*app-context* error-page]]

View file

@ -0,0 +1,11 @@
(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]))

View file

@ -0,0 +1,75 @@
(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)))

View file

@ -1,4 +1,5 @@
(ns youyesyet.routes.home (ns ^{:doc "Routes/pages available to unauthenticated users."
:author "Simon Brooke"} youyesyet.routes.home
(:require [clojure.walk :refer [keywordize-keys]] (:require [clojure.walk :refer [keywordize-keys]]
[noir.response :as nresponse] [noir.response :as nresponse]
[noir.util.route :as route] [noir.util.route :as route]
@ -8,12 +9,37 @@
[ring.util.http-response :as response] [ring.util.http-response :as response]
[clojure.java.io :as io])) [clojure.java.io :as io]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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 app-page [] (defn app-page []
(layout/render "app.html")) (layout/render "app.html"))
(defn about-page [] (defn about-page []
(layout/render "about.html")) (layout/render "about.html"))
(defn call-me-page [request] (defn call-me-page [request]
(if (if
request request
@ -25,21 +51,45 @@
;; TODO: Issues need to be fetched from the database ;; TODO: Issues need to be fetched from the database
:concerns nil}))) :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))
)))
(defn home-page [] (defn home-page []
(layout/render "home.html" {:title "You Yes Yet?"})) (layout/render "home.html" {:title "You Yes Yet?"}))
(defn login-page (defn login-page
"This is very temporary. We're going to do authentication by oauth." "This is very temporary. We're going to do authentication by oauth."
[request] [request]
(let [params (keywordize-keys (:form-params request)) (let [params (keywordize-keys (:form-params request))
session (:session request) session (:session request)
username (:username params) username (:username params)
user (if username (db-core/get-canvasser-by-username db-core/*db* {:username username}))
password (:password params) password (:password params)
redirect-to (or (:redirect-to params) "app")] redirect-to (or (:redirect-to params) "roles")]
(if (cond
(and (= username "test") (= password "test")) ;; this is obviously, ABSURDLY, insecure. I don't want to put just-about-good-enough,
(do ;; it-will-do-for-now security in place; instead, I want this to be test code only
(assoc (response/found redirect-to) :session (assoc session :user username))) ;; 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})))) (layout/render "login.html" {:title "Please log in" :redirect-to redirect-to}))))
@ -47,6 +97,7 @@
(GET "/" [] (home-page)) (GET "/" [] (home-page))
(GET "/home" [] (home-page)) (GET "/home" [] (home-page))
(GET "/about" [] (about-page)) (GET "/about" [] (about-page))
(GET "/roles" request (route/restricted (roles-page request)))
(GET "/app" [] (route/restricted (app-page))) (GET "/app" [] (route/restricted (app-page)))
(GET "/call-me" [] (call-me-page nil)) (GET "/call-me" [] (call-me-page nil))
(POST "/call-me" request (call-me-page request)) (POST "/call-me" request (call-me-page request))

View file

@ -1,4 +1,5 @@
(ns youyesyet.routes.oauth (ns ^{:doc "OAuth authentication routes - not finished, does not work yet."
:author "Simon Brooke"} youyesyet.routes.oauth
(:require [compojure.core :refer [defroutes GET]] (:require [compojure.core :refer [defroutes GET]]
[ring.util.http-response :refer [ok found]] [ring.util.http-response :refer [ok found]]
[clojure.java.io :as io] [clojure.java.io :as io]

View file

@ -1,4 +1,5 @@
(ns youyesyet.routes.services (ns ^{:doc "REST API."
:author "Simon Brooke"} youyesyet.routes.services
(:require [clj-http.client :as client] (:require [clj-http.client :as client]
[ring.util.http-response :refer :all] [ring.util.http-response :refer :all]
[compojure.api.sweet :refer :all] [compojure.api.sweet :refer :all]

View file

@ -0,0 +1,126 @@
(ns ^{:doc "Queue of messages waiting to be sent to the server."
:author "Simon Brooke"}
youyesyet.outqueue
(:require
#?(:clj [clojure.core]
:cljs [reagent.core :refer [atom]])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.outqueue: queue of messages waiting to be sent to the server.
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The items are (obviously) the actual items in the queue;
;;; the queue is locked if an attempt is currently being made to transmit
;;; an item.
(defn new-queue
"Create a new queue"
([]
(new-queue '()))
([items]
(atom {:locked false
:items (if
(seq? items)
(reverse items)
(list items))})))
(defn add!
"Add this item to the queue."
[q item]
(swap! q
(fn [a]
(assoc a :items
(cons item (:items a))))))
(defn queue?
"True if x is a queue, else false."
[x]
(try
(let [q (deref x)
locked (:locked q)]
(and
(seq? (:items q))
(or (true? locked) (false? locked))))
(catch #?(:clj Exception :cljs js/Object) any
#?(:clj (print (.getMessage any))
:cljs (js/console.log (str any))))))
(defn peek
"Look at the next item which could be removed from the queue."
[q]
(last (:items (deref q))))
(defn locked?
[q]
(:locked (deref q)))
(defn unlock!
([q ]
(unlock! q true))
([q value]
(swap! q (fn [a] (assoc a :locked (not (true? value)))))))
(defn lock!
[q]
(unlock! q false))
(defn count
"Return the count of items currently in the queue."
[q]
(count (deref q)))
(defn take!
"Return the first item from the queue, rebind the queue to the remaining
items. If the queue is empty return nil."
[q]
(swap! q (fn [a]
(let [items (reverse (:items a))
item (first items)
new-queue (reverse (rest items))]
(assoc (assoc a :items new-queue) :v item))))
(:v (deref q)))
(defn maybe-process-next
"Apply this process, assumed to be a function of one argument, to the next
item in the queue, if the queue is not currently locked; return the value
returned by process."
[q process]
(if (and (queue? q)(not (locked? q)))
(try
(lock! q)
(let [v (apply process (list (peek q)))]
(take! q)
v)
(catch #?(:clj Exception :cljs js/Object) any
#?(:clj (print (.getMessage any))
:cljs (js/console.log (str any))))
(finally (unlock! q)))
))

View file

@ -1,3 +1,26 @@
(ns youyesyet.validation (ns youyesyet.validation
(:require [bouncer.core :as b] (:require [bouncer.core :as b]
[bouncer.validators :as v])) [bouncer.validators :as v]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.validation:
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -1,20 +0,0 @@
(ns youyesyet.ajax
(:require [ajax.core :as ajax]))
(defn local-uri? [{:keys [uri]}]
(not (re-find #"^\w+?://" uri)))
(defn default-headers [request]
(if (local-uri? request)
(-> request
(update :uri #(str js/context %))
(update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
request))
(defn load-interceptors! []
(swap! ajax/default-interceptors
conj
(ajax/to-interceptor {:name "default headers"
:request default-headers})))

View file

@ -1,9 +1,11 @@
(ns youyesyet.views.building (ns ^{:doc "Canvasser app transciever for ajax packets."
(:require [re-frame.core :refer [reg-sub]])) :author "Simon Brooke"}
youyesyet.canvasser-app.ajax
(:require [ajax.core :as ajax]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.building: building view for youyesyet. ;;;; youyesyet.canvasser-app.ajax: transciever for ajax packets.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -25,13 +27,20 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have (defn local-uri? [{:keys [uri]}]
;;; one source file/namespace per view. Each namespace contains a function 'panel' (not (re-find #"^\w+?://" uri)))
;;; whose output is an enlive-style specification of the view to be redered.
;;; I propose to follow this pattern. This file will (eventually) provide the building view. (defn default-headers [request]
(if (local-uri? request)
(-> request
(update :uri #(str js/context %))
(update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
request))
(defn load-interceptors! []
(swap! ajax/default-interceptors
conj
(ajax/to-interceptor {:name "default headers"
:request default-headers})))
(defn panel
"Generate the building panel."
[]
[])

View file

@ -1,26 +1,30 @@
(ns youyesyet.core (ns ^{:doc "Canvasser app navigation and routing."
(:require [reagent.core :as r] :author "Simon Brooke"}
[re-frame.core :as rf] youyesyet.canvasser-app.core
[secretary.core :as secretary] (:require cljsjs.react-leaflet
[ajax.core :refer [GET POST]]
[goog.events :as events] [goog.events :as events]
[goog.history.EventType :as HistoryEventType] [goog.history.EventType :as HistoryEventType]
[markdown.core :refer [md->html]] [markdown.core :refer [md->html]]
[ajax.core :refer [GET POST]] [reagent.core :as r]
[youyesyet.ajax :refer [load-interceptors!]] [re-frame.core :as rf]
[youyesyet.handlers] [secretary.core :as secretary]
[youyesyet.subscriptions] [youyesyet.canvasser-app.ajax :refer [load-interceptors!]]
[youyesyet.ui-utils :as ui] [youyesyet.canvasser-app.handlers]
[youyesyet.views.about :as about] [youyesyet.canvasser-app.subscriptions]
[youyesyet.views.electors :as electors] [youyesyet.canvasser-app.ui-utils :as ui]
[youyesyet.views.followup :as followup] [youyesyet.canvasser-app.views.about :as about]
[youyesyet.views.issue :as issue] [youyesyet.canvasser-app.views.building :as building]
[youyesyet.views.issues :as issues] [youyesyet.canvasser-app.views.electors :as electors]
[youyesyet.views.map :as maps]) [youyesyet.canvasser-app.views.followup :as followup]
[youyesyet.canvasser-app.views.issue :as issue]
[youyesyet.canvasser-app.views.issues :as issues]
[youyesyet.canvasser-app.views.map :as maps])
(:import goog.History)) (:import goog.History))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.core: core of the app. ;;;; youyesyet.canvasser-app.core: core of the app.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -47,6 +51,9 @@
(defn about-page [] (defn about-page []
(about/panel)) (about/panel))
(defn building-page []
(building/panel))
(defn electors-page [] (defn electors-page []
(electors/panel)) (electors/panel))
@ -64,6 +71,7 @@
(def pages (def pages
{:about #'about-page {:about #'about-page
:building #'building-page
:electors #'electors-page :electors #'electors-page
:followup #'followup-page :followup #'followup-page
:issues #'issues-page :issues #'issues-page
@ -71,11 +79,26 @@
:map #'map-page :map #'map-page
}) })
(defn page []
(defn page
"Render the single page of the app, taking the current panel from
the :page key in the state map."
[]
(let [content (pages @(rf/subscribe [:page]))
error @(rf/subscribe [:error])
feedback @(rf/subscribe [:feedback])
outqueue @(rf/subscribe [:outqueue])]
[:div [:div
[:header [:header
[ui/navbar]] [ui/navbar]]
[(pages @(rf/subscribe [:page]))]]) (if content [content]
[:div.error (str "No content in page " :page)])
[:footer
[:div.error {:style [:display (if error "block" "none")]} (str error)]
[:div.feedback {:style [:display (if feedback :block :none)]} (str feedback)]
[:div.queue (if
(nil? outqueue) ""
(str (count outqueue) " items queued to send"))]]]))
;; ------------------------- ;; -------------------------
;; Routes ;; Routes
@ -87,8 +110,11 @@
(secretary/defroute "/about" [] (secretary/defroute "/about" []
(rf/dispatch [:set-active-page :about])) (rf/dispatch [:set-active-page :about]))
(secretary/defroute "/electors" [] (secretary/defroute "/electors/:dwelling" {dwelling-id :dwelling}
(rf/dispatch [:set-active-page :electors])) (rf/dispatch [:set-dwelling dwelling-id]))
(secretary/defroute "/building/:address" {address-id :address}
(rf/dispatch [:set-address address-id]))
(secretary/defroute "/followup" [] (secretary/defroute "/followup" []
(rf/dispatch [:set-active-page :followup])) (rf/dispatch [:set-active-page :followup]))
@ -100,7 +126,7 @@
(rf/dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}])) (rf/dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}]))
(secretary/defroute "/issue/:issue" {issue :issue} (secretary/defroute "/issue/:issue" {issue :issue}
(rf/dispatch [:set-issue issue])) (rf/dispatch [:set-and-go-to-issue issue]))
(secretary/defroute "/map" [] (secretary/defroute "/map" []
(rf/dispatch [:set-active-page :map])) (rf/dispatch [:set-active-page :map]))

View file

@ -0,0 +1,226 @@
(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]]
[youyesyet.canvasser-app.state :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.handlers: handlers for events.
;;;;
;;;; 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 clear-messages
"Return a state like this state except with the error and feedback messages
set nil"
[state]
(merge state {:error nil :feedback nil}))
(defn get-elector
"Return the elector at this address (or the current address if not specified)
with this id."
([elector-id state]
(get-elector elector-id state (:address state)))
([elector-id state address]
(first
(remove
nil?
(map
#(if (= elector-id (:id %)) %)
(:electors address))))))
(reg-event-db
:initialize-db
(fn [_ _]
db/default-db))
(reg-event-db
:send-intention
(fn [db [_ args]]
(let [intention (:intention args)
elector-id (:elector-id args)
old-elector (first
(remove nil?
(map
#(if (= elector-id (:id %)) %)
(:electors (:dwelling db)))))
new-elector (assoc old-elector :intention intention)
old-dwelling (:dwelling db)
new-dwelling (assoc
old-dwelling
:electors
(cons
new-elector
(remove
#(= % old-elector)
(:electors old-dwelling))))
old-address (:address db)
new-address (assoc
old-address
:dwellings
(cons
new-dwelling
(remove
#(= % old-dwelling)
(: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)
true
(do
(js/console.log (str "Setting intention of elector " old-elector " to " intention))
(merge
(clear-messages db)
{:addresses
(cons
new-address
(remove #(= % old-address) (:addresses db)))
:address new-address
:dwelling new-dwelling
:elector new-elector
:outqueue (cons
(assoc args :action :set-intention)
(:outqueue db))}))))))
(reg-event-db
:send-request
(fn [db [_ _]]
(if (and (:elector db) (:issue db) (:telephone db))
(do
(js/console.log "Sending request")
(assoc db
:feedback "Request has been queued"
:outqueue (cons
{:elector-id (:id (:elector db))
:issue (:issue db)
:action :add-request} (:outqueue db))))
(assoc db :error "Please supply a telephone number to call"))))
(reg-event-db
:set-active-page
(fn [db [_ page]]
(if page
(assoc (clear-messages db) :page page))))
(reg-event-db
:set-address
(fn [db [_ address-id]]
(let [id (read-string 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)
(assoc (clear-messages db)
:address address
:dwelling nil
:page :building)))))
(reg-event-db
:set-dwelling
(fn [db [_ dwelling-id]]
(let [id (read-string dwelling-id)
dwelling (first
(remove
nil?
(map
#(if (= id (:id %)) %)
(mapcat :dwellings (:addresses db)))))]
(assoc (clear-messages db) :dwelling dwelling :page :electors))))
(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)))
(reg-event-db
:set-elector-and-page
(fn [db [_ args]]
(let [page (:page args)
elector-id (read-string (: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))))
(reg-event-db
:set-elector
(fn [db [_ elector-id]]
(let [elector (get-elector (read-string elector-id) db)]
(js/console.log (str "Setting elector to " elector))
(assoc (clear-messages db) :elector elector))))
(reg-event-db
:set-issue
(fn [db [_ issue]]
(js/console.log (str "Setting issue to " issue))
(assoc (clear-messages db) :issue issue)))
(reg-event-db
:set-latitude
(fn [db [_ issue]]
(assoc db :latitude issue)))
(reg-event-db
:set-longitude
(fn [db [_ issue]]
(assoc db :longitude issue)))
(reg-event-db
:set-telephone
(fn [db [_ telephone]]
(js/console.log (str "Setting telephone to " telephone))
(assoc (clear-messages db) :telephone telephone)))
(reg-event-db
:set-view
(fn [db [_ view]]
(assoc db :view view)))
(reg-event-db
:set-zoom
(fn [db [_ zoom]]
(if (integer? zoom)
(assoc db :zoom zoom)
db)))

View file

@ -1,8 +1,10 @@
(ns youyesyet.db) (ns ^{:doc "Canvasser app client state."
:author "Simon Brooke"}
youyesyet.canvasser-app.state)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.db: the state of the app. ;;;; youyesyet.canvasser-app.state: the state of the app.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -31,24 +33,43 @@
(def default-db (def default-db
{;;; the currently selected address, if any. {;;; the currently selected address, if any.
:address {:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2590944 :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} :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female} {:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes} {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]} {:id 4 :name "Andy Anderson" :intention :yes}]}]}
;;; a list of the addresses in the current location at which there ;;; a list of the addresses in the current location at which there
;;; are electors registered. ;;; are electors registered.
:addresses [{:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2590944 :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} :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female} {:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes} {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]}] {:id 4 :name "Andy Anderson" :intention :yes}]}]}
;;; electors at the currently selected address {: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}]}]}
{: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}]}]}]
;;; electors at the currently selected dwelling
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no} :electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female} {:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes} {:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}] {:id 4 :name "Andy Anderson" :intention :yes}]
;;; any error to display
:error nil
;;; the issue from among the issues which is currently selected. ;;; the issue from among the issues which is currently selected.
;;; any confirmation message to display
:feedback nil
;;; the currently selected issue
:issue "Currency" :issue "Currency"
;;; the issues selected for the issues page on this day. ;;; 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" :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"
@ -58,6 +79,11 @@
:motd "This is a test version only. There is no real data." :motd "This is a test version only. There is no real data."
;;; the options from among which electors can select. ;;; the options from among which electors can select.
:options [{:id :yes :description "Yes"} {:id :no :description "No"}] :options [{:id :yes :description "Yes"} {:id :no :description "No"}]
;;; the currently displayed 'page' within the app. ;;; the queue of items waiting to be transmitted.
:outqueue ()
;;; the currently displayed page within the app.
:page :home :page :home
}) :view nil
:latitude 55.82
:longitude -4.25
:zoom 12})

View file

@ -1,4 +1,6 @@
(ns youyesyet.subscriptions (ns ^{:doc "Canvasser app event subscriptions."
:author "Simon Brooke"}
youyesyet.canvasser-app.subscriptions
(:require [re-frame.core :refer [reg-sub]])) (:require [re-frame.core :refer [reg-sub]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -39,11 +41,31 @@
(fn [db _] (fn [db _]
(:addresses db))) (:addresses db)))
(reg-sub
:changes
(fn [db _]
(:changes db)))
(reg-sub
:dwelling
(fn [db _]
(:dwelling db)))
(reg-sub (reg-sub
:elector :elector
(fn [db _] (fn [db _]
(:elector db))) (:elector db)))
(reg-sub
:error
(fn [db _]
(:error db)))
(reg-sub
:feedback
(fn [db _]
(:feedback db)))
(reg-sub (reg-sub
:issue :issue
(fn [db _] (fn [db _]
@ -54,6 +76,16 @@
(fn [db _] (fn [db _]
(:issues db))) (:issues db)))
(reg-sub
:latitude
(fn [db _]
(:latitude db)))
(reg-sub
:longitude
(fn [db _]
(:longitude db)))
(reg-sub (reg-sub
:page :page
(fn [db _] (fn [db _]
@ -63,3 +95,18 @@
:options :options
(fn [db _] (fn [db _]
(:options db))) (:options db)))
(reg-sub
:outqueue
(fn [db _]
(:outqueue db)))
(reg-sub
:view
(fn [db _]
(:view db)))
(reg-sub
:zoom
(fn [db _]
(:zoom db)))

View file

@ -1,4 +1,6 @@
(ns youyesyet.ui-utils (ns ^{:doc "Canvasser app user interface widgets."
:author "Simon Brooke"}
youyesyet.canvasser-app.ui-utils
(:require [reagent.core :as r] (:require [reagent.core :as r]
[re-frame.core :as rf])) [re-frame.core :as rf]))

View file

@ -1,11 +1,13 @@
(ns youyesyet.views.about (ns ^{:doc "Canvasser app about panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.about
(:require [re-frame.core :refer [reg-sub subscribe]] (:require [re-frame.core :refer [reg-sub subscribe]]
[markdown.core :refer [md->html]] [markdown.core :refer [md->html]]
[youyesyet.ui-utils :as ui])) [youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.electors: about/credits view for youyesyet. ;;;; youyesyet.canvasser-app.views.about: about/credits view for youyesyet.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -41,23 +43,23 @@
[:p.motd {:dangerouslySetInnerHTML [:p.motd {:dangerouslySetInnerHTML
{:__html (md->html motd)}}] {:__html (md->html motd)}}]
[:p [:p
[:img {:src "img/ric-logo.png" :width "24" :height "24"}] [:img {:src "img/credits/ric-logo.png" :width "24" :height "24"}]
" A project of the " " A project of the "
[:a {:href "https://radical.scot/"} "Radical Independence Campaign"]] [:a {:href "https://radical.scot/"} "Radical Independence Campaign"]]
[:p [:p
[:img {:src "img/luminus-logo.png" :alt "Luminus" :height "24" :width "24"}] [:img {:src "img/credits/luminus-logo.png" :alt "Luminus" :height "24" :width "24"}]
" Built with " " Built with "
[:a {:href "http://www.luminusweb.net/"} "Luminus Web"]] [:a {:href "http://www.luminusweb.net/"} "Luminus Web"]]
[:p [:p
[:img {:src "img/clojure-icon.gif" :alt "Clojure" :height "24" :width "24"}] [:img {:src "img/credits/clojure-icon.gif" :alt "Clojure" :height "24" :width "24"}]
" Powered by " " Powered by "
[:a {:href "http://clojure.org"} "Clojure"]] [:a {:href "http://clojure.org"} "Clojure"]]
[:p [:p
[:img {:src "img/github-logo-transparent.png" :alt "GitHub" :height "24" :width "24"}] [:img {:src "img/credits/github-logo-transparent.png" :alt "GitHub" :height "24" :width "24"}]
" Find me/fork me on " " Find me/fork me on "
[:a {:href "https://github.com/simon-brooke/youyesyet"} "GitHub"]] [:a {:href "https://github.com/simon-brooke/youyesyet"} "GitHub"]]
[:p [:p
[:img {:src "img/gnu.small.png" :alt "Free Software Foundation" :height "24" :width "24"}] [:img {:src "img/credits/gnu.small.png" :alt "Free Software Foundation" :height "24" :width "24"}]
" Licensed under the " " Licensed under the "
[:a {:href "http://www.gnu.org/licenses/gpl-2.0.html"} [:a {:href "http://www.gnu.org/licenses/gpl-2.0.html"}
"GNU General Public License v2.0"]] "GNU General Public License v2.0"]]

View file

@ -0,0 +1,54 @@
(ns ^{:doc "Canvasser app households in building panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.building
(:require [re-frame.core :refer [reg-sub subscribe]]
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.canvasser-app.views.building: building 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; 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.
;;; I propose to follow this pattern. This file will provide the building view.
(defn panel
"Generate the building panel."
[]
(let [address @(subscribe [:address])
dwellings (:dwellings address)]
[:div
[:h1 (str "Flats at " (:address address))]
[:div.container {:id "main-container"}
(ui/back-link)
[:div {:id "dwelling-list"}
(map
(fn
[dwelling]
(ui/big-link
(:sub-address dwelling)
(str "#/electors/" (:id dwelling))) )
(sort
#(< (:sub-address %1) (:sub-address %2))
(:dwellings address)))]]]))

View file

@ -1,10 +1,13 @@
(ns youyesyet.views.electors (ns ^{:doc "Canvasser app electors in household panel."
(:require [re-frame.core :refer [reg-sub subscribe]] :author "Simon Brooke"}
[youyesyet.ui-utils :as ui])) youyesyet.canvasser-app.views.electors
(:require [reagent.core :refer [atom]]
[re-frame.core :refer [reg-sub subscribe dispatch]]
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.electors: electors view for youyesyet. ;;;; youyesyet.canvasser-app.views.electors: electors view for youyesyet.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -29,7 +32,7 @@
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have ;;; 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' ;;; 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. ;;; whose output is an enlive-style specification of the view to be redered.
;;; I propose to follow this pattern. This file will (eventually) provide the electors view. ;;; I propose to follow this pattern. This file will provide the electors view.
;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#electors-view ;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#electors-view
@ -47,12 +50,14 @@
image (if gender (name gender) "unknown")] image (if gender (name gender) "unknown")]
[:td {:key (:id elector)} [:img {:src (str "img/gender/" image ".png") :alt image}]])) [:td {:key (:id elector)} [:img {:src (str "img/gender/" image ".png") :alt image}]]))
(defn genders-row (defn genders-row
[electors] [electors]
[:tr [:tr
(map (map
#(gender-cell %) electors)]) #(gender-cell %) electors)])
(defn name-cell (defn name-cell
[elector] [elector]
[:td {:key (str "name-" (:id elector))} (:name elector)]) [:td {:key (str "name-" (:id elector))} (:name elector)])
@ -63,26 +68,33 @@
(map (map
#(name-cell %) electors)]) #(name-cell %) electors)])
(defn options-row (defn options-row
[electors option] [electors option]
(let [optid (:id option) (let [optid (:id option)
optname (name optid)] optname (name optid)]
[:tr {:key (str "options-" optid)} [:tr {:key (str "options-" optname)}
(map (map
#(let [selected (= optid (:intention %)) (fn [elector] (let [selected (= optid (:intention elector))
image (if selected (str "img/option/" optname "-selected.png") image (if selected (str "img/option/" optname "-selected.png")
(str "img/option/" optname "-unselected.png"))] (str "img/option/" optname "-unselected.png"))]
[:td {:key (str "option-" optid "-" (:id %))} [:td {:key (str "option-" optid "-" (:id elector))}
[:a {:href (str "#/set-intention/" (:id %) "/" optid)} [:img
[:img {:src image :alt optname}]]]) {: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)])) electors)]))
(defn issue-cell (defn issue-cell
"Create an issue cell for a particular elector" "Create an issue cell for a particular elector"
[elector] [elector]
[:td {:key (:id elector)} [:td {:key (:id elector)}
[:a {:href (str "#/issues/" (:id elector))} [:a {:href (str "#/issues/" (:id elector))}
[:img {:src "/img/issues.png" :alt "Issues"}]]]) [:img {:src "img/issues.png" :alt "Issues"}]]])
(defn issues-row (defn issues-row
@ -95,13 +107,16 @@
(defn panel (defn panel
"Generate the electors panel." "Generate the electors panel."
[] []
(let [address @(subscribe [:address]) (let [dwelling @(subscribe [:dwelling])
addresses @(subscribe [:addresses]) address @(subscribe [:address])
electors (:electors address) sub-address (:sub-address dwelling)
electors (sort-by :id (:electors dwelling))
options @(subscribe [:options])] options @(subscribe [:options])]
(if address (if address
[:div [:div
[:h1 (:address address)] [:h1 (if sub-address
(str sub-address ", " (:address address))
(:address address))]
[:div.container {:id "main-container"} [:div.container {:id "main-container"}
[:table [:table
[:tbody [:tbody

View file

@ -1,10 +1,12 @@
(ns youyesyet.views.followup (ns ^{:doc "Canvasser followup request form panel."
(:require [re-frame.core :refer [reg-sub subscribe]] :author "Simon Brooke"}
[youyesyet.ui-utils :as ui])) youyesyet.canvasser-app.views.followup
(:require [re-frame.core :refer [reg-sub subscribe dispatch]]
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.followup-request: followup-request view for youyesyet. ;;;; youyesyet.canvasser-app.views.followup-request: followup-request view for youyesyet.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -39,36 +41,39 @@
(let [issue @(subscribe [:issue]) (let [issue @(subscribe [:issue])
issues @(subscribe [:issues]) issues @(subscribe [:issues])
elector @(subscribe [:elector]) elector @(subscribe [:elector])
address @(subscribe [:address])] dwelling @(subscribe [:dwelling])]
(js/console.log (str "Issue is " issue "; elector is " elector)) (js/console.log (str "Issue is " issue "; elector is " elector))
(cond (cond
(nil? address) (nil? dwelling)
(ui/error-panel "No address selected") (ui/error-panel "No dwelling selected")
(nil? issues) (nil? issues)
(ui/error-panel "No issues loaded") (ui/error-panel "No issues loaded")
true true
[:div [:div
[:h1 "Followup Request"] [:h1 "Followup Request"]
[:div.container {:id "main-container"} [:div.container {:id "main-container"}
[:form {} [:div {}
[:p.widget [:p.widget
[:label {:for "elector"} "Elector"] [:label {:for "elector"} "Elector"]
[:select {:id "elector" :name "elector" :value (:id elector)} [:select {:id "elector" :name "elector" :defaultValue (:id elector)
:on-change #(dispatch [:set-elector (.-value (.-target %))])}
(map (map
#(let [] #(let []
[:option {:value (:id %) :key (:id %)} (:name %)]) (:electors address))]] [:option {:value (:id %) :key (:id %)} (:name %)]) (:electors dwelling))]]
[:p.widget [:p.widget
[:label {:for "issue"} "Issue"] [:label {:for "issue"} "Issue"]
[:select {:id "issue" :name "issue" :value issue} ;; #(reset! val (-> % .-target .-value))
[:select {:id "issue" :name "issue" :defaultValue issue
:on-change #(dispatch [:set-issue (.-value (.-target %))])}
(map (map
#(let [] #(let []
[:option {:key % :value %} %]) (keys issues))]] [:option {:key % :value %} %]) (keys issues))]]
[:p.widget [:p.widget
[:label {:for "telephone"} "Telephone number"] [:label {:for "telephone"} "Telephone number"]
[:input {:type "text" :id "telephone" :name "telephone"}]] [:input {:type "text" :id "telephone" :name "telephone"
:on-change #(dispatch [:set-telephone (.-value (.-target %))])}]]
[:p.widget [:p.widget
[:label {:for "submit"} "To request a call"] [:label {:for "send"} "To request a call"]
[:input {:id "submit" :name "submit" :type "submit" :value "Send this!"}]] [:button {:id "send" :on-click #(dispatch [:send-request])} "Send this!"]]]
]
(ui/back-link)]]))) (ui/back-link)]])))

View file

@ -1,8 +1,10 @@
(ns youyesyet.views.issue (ns ^{:doc "Canvasser app current issue detail panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.issue
(:require [re-frame.core :refer [reg-sub subscribe]] (:require [re-frame.core :refer [reg-sub subscribe]]
[markdown.core :refer [md->html]] [markdown.core :refer [md->html]]
[youyesyet.ui-utils :as ui] [youyesyet.canvasser-app.ui-utils :as ui]
[youyesyet.views.issues :as issues])) [youyesyet.canvasser-app.views.issues :as issues]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;

View file

@ -1,10 +1,12 @@
(ns youyesyet.views.issues (ns ^{:doc "Canvasser app current issues list panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.issues
(:require [re-frame.core :refer [reg-sub subscribe]] (:require [re-frame.core :refer [reg-sub subscribe]]
[youyesyet.ui-utils :as ui])) [youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.issues: issues view for youyesyet. ;;;; youyesyet.canvasser-app.views.issues: issues view for youyesyet.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -29,7 +31,7 @@
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have ;;; 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' ;;; 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. ;;; whose output is an enlive-style specification of the view to be redered.
;;; I propose to follow this pattern. This file will (eventually) provide the issues view. ;;; I propose to follow this pattern. This file will provide the issues view.
;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#issues-view ;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#issues-view

View file

@ -1,10 +1,12 @@
(ns youyesyet.views.map (ns ^{:doc "Canvasser app map view panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.map
(:require [re-frame.core :refer [reg-sub subscribe dispatch]] (:require [re-frame.core :refer [reg-sub subscribe dispatch]]
[reagent.core :as reagent])) [reagent.core :as reagent]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; youyesyet.views.map: map view for youyesyet. ;;;; youyesyet.canvasser-app.views.map: map view for youyesyet.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -55,17 +57,36 @@
(defn pin-image (defn pin-image
"select the name of a suitable pin image for this address" "select the name of a suitable pin image for this address"
[address] [address]
(let [intentions (set (remove nil? (map #(:intention %) (:electors address))))] (let [intentions
(set
(remove
nil?
(map
:intention
(mapcat :electors
(:dwellings address)))))]
(case (count intentions) (case (count intentions)
0 "unknown-pin" 0 "unknown-pin"
1 (str (name (first intentions)) "-pin") 1 (str (name (first intentions)) "-pin")
"mixed-pin"))) "mixed-pin")))
(defn click-handler (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] [id]
(js/console.log (str "Click handler for address #" id)) (js/console.log (str "Click handler for address #" id))
(dispatch [:set-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 (defn add-map-pin
@ -83,9 +104,10 @@
:shadowAnchor [16 23]})) :shadowAnchor [16 23]}))
marker (.marker js/L marker (.marker js/L
(.latLng js/L lat lng) (.latLng js/L lat lng)
(clj->js {:icon pin :title (:address address)})) (clj->js {:icon pin
:title (:address address)}))
] ]
(.on marker "click" #(fn [] (click-handler (:id address)))) (.on marker "click" (fn [_] (map-pin-click-handler (str (:id address)))))
(.addTo marker view))) (.addTo marker view)))
@ -104,7 +126,10 @@
(defn map-did-mount-osm (defn map-did-mount-osm
"Did-mount function loading map tile data from Open Street Map." "Did-mount function loading map tile data from Open Street Map."
[] []
(let [view (.setView (.map js/L "map" (clj->js {:zoomControl false})) #js [55.82 -4.25] 13) (let [view (.setView
(.map js/L "map" (clj->js {:zoomControl false}))
#js [@(subscribe [:latitude]) @(subscribe [:longitude])]
@(subscribe [:zoom]))
addresses @(subscribe [:addresses])] addresses @(subscribe [:addresses])]
(js/console.log (str "Adding " (count addresses) " pins")) (js/console.log (str "Adding " (count addresses) " pins"))
(doall (map #(add-map-pin % view) addresses)) (doall (map #(add-map-pin % view) addresses))
@ -112,7 +137,8 @@
(clj->js {:attribution osm-attrib (clj->js {:attribution osm-attrib
:maxZoom 18})) :maxZoom 18}))
view) view)
)) (dispatch [:set-view view])
view))
(defn map-did-mount (defn map-did-mount

View file

@ -1,91 +0,0 @@
(ns youyesyet.handlers
(:require [cljs.reader :refer [read-string]]
[re-frame.core :refer [dispatch reg-event-db]]
[youyesyet.db :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.handlers: handlers for events.
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(reg-event-db
:initialize-db
(fn [_ _]
db/default-db))
(reg-event-db
:set-active-page
(fn [db [_ page]]
(assoc db :page page)))
(reg-event-db
:set-address
(fn [db [_ address-id]]
(let [id (read-string address-id)
address (first (remove nil? (map #(if (= id (:id %)) %) (:addresses db))))]
(assoc (assoc db :address address) :page :electors))))
(reg-event-db
:set-elector-and-page
(fn [db [_ args]]
(let [page (:page args)
elector-id (read-string (:elector-id args))
elector
(first
(remove nil?
(map
#(if (= elector-id (:id %)) %)
(:electors (:address db)))))]
(js/console.log (str "Setting page to " page ", elector to " elector))
(assoc (assoc db :elector elector) :page page))))
(reg-event-db
:set-intention
(fn [db [_ args]]
(let [intention (:intention args)
elector-id (read-string (:elector-id args))
elector
(first
(remove nil?
(map
#(if (= elector-id (:id %)) %)
(:electors (:address db)))))
old-address (:address db)
new-address (assoc old-address :electors (cons (assoc elector :intention intention) (remove #(= % elector) (:electors old-address))))]
(cond
(nil? elector)(do (js/console.log "No elector found; not setting intention") db)
(= intention (:intention 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 " elector " to " intention))
(assoc db :addresses (cons new-address (remove old-address (:addresses db)))))))))
(reg-event-db
:set-issue
(fn [db [_ issue]]
(js/console.log (str "Setting page to :issue, issue to " issue))
(assoc (assoc db :issue issue) :page :issue)))

View file

@ -1,7 +1,7 @@
(ns youyesyet.core-test (ns youyesyet.canvasser-app.core-test
(:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]]
[reagent.core :as reagent :refer [atom]] [reagent.core :as reagent :refer [atom]]
[youyesyet.core :as rc])) [youyesyet.canvasser-app.core :as rc]))
(deftest test-home (deftest test-home
(is (= true true))) (is (= true true)))

View file

@ -0,0 +1,6 @@
(ns youyesyet.canvasser-app.doo-runner
(:require [doo.runner :refer-macros [doo-tests]]
[youyesyet.canvasser-app.core-test]))
(doo-tests 'youyesyet.canvasser-app.canvasser-app.core-test)

View file

@ -1,6 +0,0 @@
(ns youyesyet.doo-runner
(:require [doo.runner :refer-macros [doo-tests]]
[youyesyet.core-test]))
(doo-tests 'youyesyet.core-test)