0.1.0 First public alpha

This commit is contained in:
Simon Brooke 2018-07-18 23:31:28 +01:00
commit b275000632
112 changed files with 6154 additions and 8322 deletions

18
.gitignore vendored
View file

@ -10,6 +10,7 @@ pom.xml.asc
/resources/public/content/.git
/resources/public/vendor
/bower_components/
/resources/public/js/lib/
.lein-deps-sum
.lein-repl-history
.lein-plugins/
@ -21,4 +22,19 @@ pom.xml.asc
*-init.clj
profiles\.clj
.bowerrc
bower.json
bower.json
.idea
src/clj/youyesyet/scratchpad\.clj
*.iml
resources/sql/queries\.auto\.sql
resources/templates/auto/
src/clj/youyesyet/routes/auto\.clj
src/clj/youyesyet/routes/auto_json\.clj
resources/sql/youyesyet\.postgres\.sql

View file

@ -59,16 +59,24 @@ Do get the database initialised, run
createdb youyesyet_dev
followed by
I'm no longer using Migratus as I'm using [Application Description Language]()
to generate the majority of the application, and, as changes are made to the application
description, new database schemas are generated. The database initialisation script will
be found at `resources/sql/youyesyet.postgres.sql`. Reference data initialisation scripts
will in due course be stored in the same directory.
lein migratus migrate
**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).
Once we have a more or less finished application it may be worth going back to
[Migratus](https://github.com/yogthos/migratus); I might have a go at generating migrations from
diffs between successive versions of the application description.
## Running in a dev environment
To run in a dev environment, checkout the *develop* branch
To download and install Javascript delendencies, run
lein npm install
To start a development web server for the application, run:
lein run

View file

@ -0,0 +1,23 @@
#%RAML 0.8
---
title: YouYesYet API
baseUri: https://api.yyy.scot/{version}
version: v1
/canvassers:
get:
put:
/electors:
get:
/{id}:
get:
/{address_id}:
get:
# Location isn't a real entity in the database, but it is a means of searching for
# addresses and electors.
/location:
/{lat}/{long}/{radius}:
get:

View file

@ -0,0 +1,17 @@
# Security and authorisation
Essentially we have six levels of authorisation, at essentially increasing levels of sensitivity.
1. *Canvassers:* Any authenticated user essentially has this level of authorisation. Hence users of the app can all share the same database connections without problem. Therefore there will be one first-class database user for all canvassers, and they will not have individual real database logins.
2. *Issue experts:* Issue experts respond to followup requests. Therefore they must be able to see the queue of requests and the details of the elector making the request. They don't need to see voter intentions and I don't believe the information they do need to see is particularly sensitive. So they too can share a single database-layer login and connection pool; whether this is the same login as used by the canvassers is an implementation detail but I don't believe that it's critical.
3. *Issue editors:* Don't need to see much sensitive data (although they do need to see, in aggregate, what issues are being raised by electors in the field), but they do have the power to dictate the initial responses canvassers make to issues raised, so the information they can *write* is pretty sensitive. We need to be very sure that unauthorised users don't have the power to write this data. So I suggest that issue editors probably should have individual first class database logins.
4. *Team leaders:* Need to be able to monitor the performance of their teams, to invite new users to the system and to block abusive users from the system. Again, these are significant functions which should be well protected from abuse. But we will have at least hundreds, probably thousands of team leaders across Scotland. I would prefer that they each had first class logins, but this may be impractical. But in any case, even if they use a shared login, it should not be the same shared login as used by canvassers.
5. *Analyists* Need broad authorisation to read, but not write or edit, all sensitive data held by the system. They must have individual first class database logins.
6. *Admins* Can necessarily read and write everything. They should definitely each have individual first class database logins.
This means we have a hybrid authentication scheme; for lower levels, application layer security and shared connection pools are adequate. For higher levels, individual connections and database layer authorisation are required. It implies that the routes at the different layers should be separated into separate namespaces with separate authentication functions.

View file

@ -14,7 +14,7 @@
viewBox="0 0 1052.3622 744.09448"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="entity-relationship-diagram.svg">
<defs
id="defs4" />
@ -25,16 +25,16 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="455.50968"
inkscape:cy="346.1212"
inkscape:zoom="0.9899495"
inkscape:cx="472.36875"
inkscape:cy="325.73865"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1920"
inkscape:window-height="1031"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-height="1043"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
@ -62,73 +62,63 @@
id="matte"
width="1030"
height="730"
x="8.484766"
y="312.36221"
sodipodi:insensitive="true" />
x="16.060907"
y="312.36218" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="36.365494"
y="354.3468"
id="text4136"
sodipodi:linespacing="125%"><tspan
y="335.1539"
id="text4136"><tspan
sodipodi:role="line"
id="tspan4138"
x="36.365494"
y="354.3468" /></text>
y="335.1539"
style="font-size:20px;line-height:1.25"> </tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="470.03485"
y="379.60062"
id="text4140"
sodipodi:linespacing="125%"><tspan
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="117.1777"
y="360.40771"
id="text4140"><tspan
sodipodi:role="line"
id="tspan4142"
x="470.03485"
y="379.60062"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:'Arial Bold'">YouYesYet: Entity Relationship Diagram</tspan></text>
<g
id="g4382">
<rect
y="430.84955"
x="48.459385"
height="60"
width="100"
id="rect4146"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4148"
y="460.84955"
x="58.459385"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="460.84955"
x="58.459385"
id="tspan4150"
sodipodi:role="line">District</tspan></text>
</g>
x="117.1777"
y="360.40771"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:1.25;font-family:Arial;-inkscape-font-specification:'Arial Bold'">YouYesYet: Entity Relationship Diagram</tspan></text>
<rect
y="553.07794"
x="220.18532"
y="411.65665" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="230.18532"
y="441.65665"
id="text4148"><tspan
sodipodi:role="line"
id="tspan4150"
x="230.18532"
y="441.65665"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">District</tspan></text>
<rect
y="533.88507"
x="220.18532"
height="60"
width="100"
id="rect4152"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4154"
y="583.078"
x="230.18532"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="583.078"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="563.88513"
x="230.18532"
id="tspan4156"
sodipodi:role="line">Dwelling</tspan></text>
sodipodi:role="line">Address</tspan></text>
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4158"
@ -138,16 +128,15 @@
y="672.276" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="230.18532"
y="702.276"
id="text4160"
sodipodi:linespacing="125%"><tspan
y="683.08313"
id="text4160"><tspan
sodipodi:role="line"
id="tspan4162"
x="230.18532"
y="702.276"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Elector</tspan></text>
y="683.08313"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Elector</tspan></text>
<rect
y="672.276"
x="732.33264"
@ -156,29 +145,28 @@
id="rect4164"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4166"
y="702.276"
x="742.33264"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="702.276"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="683.08313"
x="742.33264"
id="tspan4168"
sodipodi:role="line">Canvasser</tspan></text>
<flowRoot
xml:space="preserve"
id="flowRoot4170"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
transform="translate(0,19.192898)"><flowRegion
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0.01%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"><flowRegion
id="flowRegion4172"><rect
id="rect4174"
width="150"
height="100"
x="20"
y="174.09448" /></flowRegion><flowPara
id="flowPara4176" /></flowRoot> <rect
id="flowPara4176"
style="font-size:20px;line-height:1.25"> </flowPara></flowRoot> <rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4178"
width="100"
@ -187,16 +175,15 @@
y="553.07794" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="742.33264"
y="583.078"
id="text4180"
sodipodi:linespacing="125%"><tspan
y="563.88513"
id="text4180"><tspan
sodipodi:role="line"
id="tspan4182"
x="742.33264"
y="583.078"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Authority</tspan></text>
y="563.88513"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Authority</tspan></text>
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4184"
@ -206,16 +193,15 @@
y="553.07794" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="400.90109"
y="583.078"
id="text4186"
sodipodi:linespacing="125%"><tspan
y="563.88513"
id="text4186"><tspan
sodipodi:role="line"
id="tspan4188"
x="400.90109"
y="583.078"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Visit</tspan></text>
y="563.88513"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Visit</tspan></text>
<rect
y="672.276"
x="390.90109"
@ -224,14 +210,13 @@
id="rect4190"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4192"
y="702.276"
x="400.90109"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="702.276"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="683.08313"
x="400.90109"
id="tspan4194"
sodipodi:role="line">Intention</tspan></text>
@ -244,20 +229,19 @@
y="794.50446" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="742.33264"
y="824.50446"
id="text4198"
sodipodi:linespacing="125%"><tspan
y="805.31158"
id="text4198"><tspan
sodipodi:role="line"
id="tspan4200"
x="742.33264"
y="824.50446"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Issue</tspan><tspan
y="805.31158"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Issue</tspan><tspan
sodipodi:role="line"
x="742.33264"
y="843.25446"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="824.06158"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
id="tspan4208">Expertise</tspan></text>
<rect
y="917.74304"
@ -267,14 +251,13 @@
id="rect4202"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4204"
y="947.74304"
x="742.33264"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="947.74304"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="928.55017"
x="742.33264"
id="tspan4206"
sodipodi:role="line">Issue</tspan></text>
@ -286,19 +269,18 @@
id="rect4221"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4223"
y="822.48413"
x="230.18532"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="822.48413"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="803.29126"
x="230.18532"
id="tspan4225"
sodipodi:role="line">Followup</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
y="841.23413"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="822.04126"
x="230.18532"
sodipodi:role="line"
id="tspan4227">Request</tspan></text>
@ -311,21 +293,20 @@
y="915.72272" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="230.18532"
y="945.72272"
id="text4231"
sodipodi:linespacing="125%"><tspan
y="926.52985"
id="text4231"><tspan
sodipodi:role="line"
id="tspan4233"
x="230.18532"
y="945.72272"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Followup</tspan><tspan
y="926.52985"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Followup</tspan><tspan
id="tspan4235"
sodipodi:role="line"
x="230.18532"
y="964.47272"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial">Action</tspan></text>
y="945.27985"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Action</tspan></text>
<g
id="g4243"
transform="translate(170.18532,48.487322)">
@ -535,13 +516,12 @@
id="rect4323"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4325"
y="683.08313"
x="570.60675"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
y="683.08313"
x="570.60675"
id="tspan4327"
@ -609,37 +589,36 @@
sodipodi:nodetypes="ccc" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="472.0152"
y="413.67694"
id="text4361"
sodipodi:linespacing="125%"><tspan
y="394.48404"
id="text4361"><tspan
sodipodi:role="line"
id="tspan4363"
x="472.0152"
y="413.67694"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"><tspan
y="394.48404"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:'Arial Bold'"
id="tspan4371">Version: </tspan>0.4</tspan><tspan
sodipodi:role="line"
x="472.0152"
y="432.42694"
y="419.48404"
id="tspan4365"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:'Arial Bold'"
id="tspan4373">Date: </tspan>20170401</tspan><tspan
sodipodi:role="line"
x="472.0152"
y="451.17694"
y="444.48404"
id="tspan4367"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:'Arial Bold'"
id="tspan4375">Author: </tspan>Simon Brooke</tspan><tspan
sodipodi:role="line"
x="472.0152"
y="469.92694"
y="469.48404"
id="tspan4369"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:'Arial Bold'"
id="tspan4377">Copyright:</tspan> (c) 2016 Simon Brooke for Radical Independence Campaign</tspan></text>
<path
@ -660,11 +639,10 @@
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="670"
y="781.55505"
id="text4385"
sodipodi:linespacing="125%"><tspan
y="762.36218"
id="text4385"><tspan
sodipodi:role="line"
id="tspan4387"
x="670"
@ -672,11 +650,10 @@
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Introduced</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="530"
y="517.5119"
id="text4389"
sodipodi:linespacing="125%"><tspan
y="498.31903"
id="text4389"><tspan
sodipodi:role="line"
id="tspan4391"
x="530"
@ -684,70 +661,64 @@
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Visited</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="450"
y="651.55505"
id="text4393"
sodipodi:linespacing="125%"><tspan
y="632.36218"
id="text4393"><tspan
sodipodi:role="line"
id="tspan4395"
x="450"
y="651.55505"
y="632.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Recorded</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="49.187798"
y="391.35205"
id="text4397"
sodipodi:linespacing="125%"><tspan
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="130"
y="392.36221"
id="text4397"><tspan
sodipodi:role="line"
id="tspan4399"
x="49.187798"
y="391.35205"
x="130"
y="392.36221"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Bold Italic';text-align:start;writing-mode:lr-tb;text-anchor:start">Raised</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="790"
y="641.55505"
id="text4401"
sodipodi:linespacing="125%"><tspan
y="622.36218"
id="text4401"><tspan
sodipodi:role="line"
id="tspan4403"
x="790"
y="641.55505"
y="622.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Authenticates</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="790"
y="761.55505"
id="text4405"
sodipodi:linespacing="125%"><tspan
y="742.36218"
id="text4405"><tspan
sodipodi:role="line"
id="tspan4407"
x="790"
y="761.55505"
y="742.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Has</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="790"
y="881.55505"
id="text4409"
sodipodi:linespacing="125%"><tspan
y="862.36218"
id="text4409"><tspan
sodipodi:role="line"
id="tspan4411"
x="790"
y="881.55505"
y="862.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">About</tspan></text>
<text
sodipodi:linespacing="125%"
id="text4413"
y="799.73273"
x="337.45166"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start"
y="799.73273"
@ -756,92 +727,80 @@
sodipodi:role="line">About</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="280"
y="891.55505"
id="text4417"
sodipodi:linespacing="125%"><tspan
y="872.36218"
id="text4417"><tspan
sodipodi:role="line"
id="tspan4419"
x="280"
y="891.55505"
y="872.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Responded to</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="161.30452"
y="480.12863"
id="text4421"
sodipodi:linespacing="125%"><tspan
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="330"
y="702.36218"
id="text4421"><tspan
sodipodi:role="line"
id="tspan4423"
x="161.30452"
y="480.12863"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Contains</tspan><tspan
sodipodi:role="line"
x="161.30452"
y="492.62863"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start"
id="tspan4389" /></text>
x="330"
y="702.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Expressed</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="280"
y="531.55505"
id="text4425"
sodipodi:linespacing="125%"><tspan
y="512.36218"
id="text4425"><tspan
sodipodi:role="line"
id="tspan4427"
x="280"
y="531.55505"
y="512.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Contains</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="280"
y="651.55505"
id="text4429"
sodipodi:linespacing="125%"><tspan
y="632.36218"
id="text4429"><tspan
sodipodi:role="line"
id="tspan4431"
x="280"
y="651.55505"
y="632.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Resides at</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="280"
y="761.55505"
id="text4433"
sodipodi:linespacing="125%"><tspan
y="742.36218"
id="text4433"><tspan
sodipodi:role="line"
id="tspan4435"
x="280"
y="761.55505"
y="742.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Requested</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="280"
y="1021.5551"
id="text4441"
sodipodi:linespacing="125%"><tspan
y="1002.3622"
id="text4441"><tspan
sodipodi:role="line"
id="tspan4443"
x="280"
y="1021.5551"
y="1002.3622"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Performed</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="340"
y="571.55505"
id="text4445"
sodipodi:linespacing="125%"><tspan
y="552.36218"
id="text4445"><tspan
sodipodi:role="line"
id="tspan4447"
x="340"
y="571.55505"
y="552.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">To</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
@ -857,11 +816,10 @@
y="672.276" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="908.88831"
y="701.55505"
id="text4285"
sodipodi:linespacing="125%"><tspan
y="682.36218"
id="text4285"><tspan
sodipodi:role="line"
id="tspan4287"
x="908.88831"
@ -883,47 +841,47 @@
y="792.48413" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="920"
y="581.55505"
id="text4302"
sodipodi:linespacing="125%"><tspan
y="562.36218"
id="text4302"><tspan
sodipodi:role="line"
id="tspan4304"
x="920"
y="581.55505" /></text>
y="562.36218"
style="font-size:20px;line-height:1.25"> </tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:15px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="908.88831"
y="821.55505"
id="text4306"
sodipodi:linespacing="125%"><tspan
y="802.36218"
id="text4306"><tspan
sodipodi:role="line"
id="tspan4308"
x="908.88831"
y="821.55505"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">Organiser-</tspan><tspan
y="802.36218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial">Organiser-</tspan><tspan
sodipodi:role="line"
x="908.88831"
y="840.30505"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial"
y="821.11218"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial"
id="tspan4310">ship</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="908.88831"
y="581.55505"
id="text4312"
sodipodi:linespacing="125%"><tspan
y="562.36218"
id="text4312"><tspan
sodipodi:role="line"
id="tspan4314"
x="908.88831"
y="581.55505">Team</tspan><tspan
y="562.36218"
style="font-size:15px;line-height:1.25">Team</tspan><tspan
sodipodi:role="line"
x="908.88831"
y="600.30505"
id="tspan4385">Membership</tspan></text>
y="581.11218"
id="tspan4385"
style="font-size:15px;line-height:1.25">Membership</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 830,691.55511 40,0 0,-110 30,0"
@ -975,11 +933,10 @@
id="path4338"
inkscape:connector-curvature="0" />
<text
sodipodi:linespacing="125%"
id="text4340"
y="762.56519"
x="879.90356"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start"
y="762.56519"
@ -988,11 +945,10 @@
sodipodi:role="line">Has</tspan></text>
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:0%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="879.90356"
y="640.33673"
id="text4344"
sodipodi:linespacing="125%"><tspan
y="621.14386"
id="text4344"><tspan
sodipodi:role="line"
id="tspan4346"
x="879.90356"
@ -1000,28 +956,26 @@
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start">Has</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="960"
y="641.55505"
id="text4348"
sodipodi:linespacing="125%"><tspan
y="622.36218"
id="text4348"><tspan
sodipodi:role="line"
id="tspan4350"
x="960"
y="641.55505"
style="font-size:10px">of</tspan></text>
y="622.36218"
style="font-size:10px;line-height:1.25">of</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="960"
y="761.55505"
id="text4352"
sodipodi:linespacing="125%"><tspan
y="742.36218"
id="text4352"><tspan
sodipodi:role="line"
id="tspan4354"
x="960"
y="761.55505"
style="font-size:10px">of</tspan></text>
y="742.36218"
style="font-size:10px;line-height:1.25">of</tspan></text>
<g
id="g4375"
transform="matrix(1,0,0,-1,171.72593,1625.9208)">
@ -1047,16 +1001,15 @@
</g>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="450"
y="761.55505"
id="text4386"
sodipodi:linespacing="125%"><tspan
y="742.36218"
id="text4386"><tspan
sodipodi:role="line"
id="tspan4388"
x="450"
y="761.55505"
style="font-size:10px">For</tspan></text>
y="742.36218"
style="font-size:10px;line-height:1.25">For</tspan></text>
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4390"
@ -1066,19 +1019,20 @@
y="672.276" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="570"
y="701.55505"
id="text4394"
sodipodi:linespacing="125%"><tspan
y="682.36218"
id="text4394"><tspan
sodipodi:role="line"
id="tspan4396"
x="570"
y="701.55505">Role</tspan><tspan
y="682.36218"
style="font-size:15px;line-height:1.25">Role</tspan><tspan
sodipodi:role="line"
x="570"
y="720.30505"
id="tspan4383">Membership</tspan></text>
y="701.11218"
id="tspan4383"
style="font-size:15px;line-height:1.25">Membership</tspan></text>
<g
id="g4354"
transform="translate(0,-222.73864)">
@ -1090,16 +1044,16 @@
x="561.61688"
y="774.30139" />
<text
sodipodi:linespacing="125%"
id="text4398"
y="802.36218"
x="570"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
y="802.36218"
x="570"
id="tspan4400"
sodipodi:role="line">Role</tspan></text>
sodipodi:role="line"
style="font-size:15px;line-height:1.25">Role</tspan></text>
</g>
<g
id="g4338"
@ -1127,28 +1081,26 @@
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="690"
y="696.55505"
id="text4346"
sodipodi:linespacing="125%"><tspan
y="677.36218"
id="text4346"><tspan
sodipodi:role="line"
id="tspan4348"
x="690"
y="696.55505"
style="font-size:10px">Is</tspan></text>
y="677.36218"
style="font-size:10px;line-height:1.25">Is</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:Arial;-inkscape-font-specification:'Arial, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="619.09393"
y="650.43823"
id="text4350"
sodipodi:linespacing="125%"><tspan
y="631.24536"
id="text4350"><tspan
sodipodi:role="line"
id="tspan4352"
x="619.09393"
y="650.43823"
style="font-size:10px">Includes</tspan></text>
y="631.24536"
style="font-size:10px;line-height:1.25">Includes</tspan></text>
<g
id="g4359"
transform="translate(341.43156,-222.73861)">

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -56,8 +56,38 @@ All this normalisation and memoisation reduces the number of read requests on th
Note that [clojure.core.memoize](https://github.com/clojure/core.memoize) provides us with functions to create both size-limited, least-recently-used caches and duration limited, time-to-live caches.
### Searching the database for localities
At 56 degrees north there are 111,341 metres per degree of latitude, 62,392 metres per degree of longitude. So a 100 metre box is about 0.0016 degrees east-west and .0009 degrees north-south. If we simplify that slightly (and we don't need square boxes, we need units of area covering a group of people working together) then we can take .001 of a degree in either direction which is computationally cheap.
Of course we could have a search query like this
select * from addresses
where latitude > 56.003
and latitude < 56.004
and longitude > -4.771
and longitude < -4.770;
And it would work - but it would be computationally expensive. If we call each of these .001 x .001 roughly-rectangles a **locality**, then we can give every locality an integer index as follows
(defn locality-index
"Compute a locality for this `latitude`, `longitude` pair."
[latitude longitude]
(+
(* 10000 ;; left-shift the latitude component four digits
(integer
(* latitude 1000)))
(- ;; invert the sign of the longitude component, since
;; we're interested in localities West of Greenwich.
(integer
(* longitude 1000)))))
For values in Scotland, this gives us a number comfortable smaller than the maximum size of a 32 bit integer. Note that this isn't generally the case, so to adapt this software for use in Canada, for example, a more general solution would need to be chosen; but this will do for now. If we compute this index at the time the address is geocoded, then we can achieve the exact same results as the query given above with a much simpler query:
select * from address where locality = 560034770;
If the locality field is indexed (which obviously it should be) this query becomes very cheap.
### Geographic sharding
Volunteers canvassing simultaneously in the same street or the same locality need to see in near real time which dwellings have been canvassed by other volunteers, otherwise we'll get the same households canvassed repeatedly, which wastes volunteer time and annoys voters. So they all need to be sending updates to, and receiving updates from, the same server. But volunteers canvassing in Aberdeen don't need to see in near real time what is happening in Edinburgh.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -1,454 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 744.09448819 1052.3622047"
id="svg4435"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="occupants.svg"
inkscape:export-filename="/home/simon/workspace/youyesyet/dummies/occupants.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300">
<defs
id="defs4437" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="51.779153"
inkscape:cy="490.59499"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1920"
inkscape:window-height="996"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4987" />
</sodipodi:namedview>
<metadata
id="metadata4440">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5308"
width="700"
height="1020"
x="20"
y="12.362205" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:45px;line-height:125%;font-family:'Arial Black';-inkscape-font-specification:'Arial Black, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="81.428566"
y="85.219345"
id="text4983"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan4985"
x="81.428566"
y="85.219345">43 Imaginary Terrace</tspan></text>
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 151.42857,208.07649 c 0,0 -22.38786,-25.28537 -20,-40 3.02043,-18.6127 21.14382,-40 40,-40 18.85618,0 36.97957,21.3873 40,40 2.38786,14.71463 -20,40 -20,40 0,0 12.56519,-3.0796 20,0 7.43481,3.0796 9.36405,0.69304 20,20 10.63595,19.30696 22.85714,101.42857 22.85714,101.42857 l -20,0 -22.85714,-81.42857 0,78.57143 20,161.42857 -40,0 -20,-120 -20,120 -40,0 17.14286,-158.57143 2.85714,-81.42857 -17.14286,80 -19.999998,0 c 0,0 7.203708,-80.79574 17.142858,-100 9.93915,-19.20426 12.56519,-16.9204 20,-20 7.43481,-3.0796 20,0 20,0"
id="path4989"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csssczzccccccccccccczzc" />
<path
sodipodi:nodetypes="csssczzccccccccccccccccczzc"
inkscape:connector-curvature="0"
id="path4991"
d="m 351.42857,208.07649 c 0,0 -22.38786,-25.28537 -20,-40 3.02043,-18.6127 21.14382,-40 40,-40 18.85618,0 36.97957,21.3873 40,40 2.38786,14.71463 -20,40 -20,40 0,0 12.56519,-3.0796 20,0 7.43481,3.0796 9.36405,0.69304 20,20 10.63595,19.30696 22.85714,101.42857 22.85714,101.42857 l -20,0 -22.85714,-81.42857 0,78.57143 C 408.57143,353.55268 430,380.45744 427.14286,407.3622 l -24.28572,-2.5 -8.57143,83.21429 c -13.50649,0 -41.87328,0 -22.85714,0 l 0,-77.14286 -1.42857,78.57143 -21.42857,-2.85714 -10.71429,-76.42857 -23.57143,-2.14286 c -3.33333,-26.19048 17.61905,-52.38095 14.28572,-78.57143 l 2.85714,-81.42857 -17.14286,80 -20,0 c 0,0 7.20371,-80.79574 17.14286,-100 9.93915,-19.20426 12.56519,-16.9204 20,-20 7.43481,-3.0796 20,0 20,0"
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 548.57143,208.07649 c 0,0 -22.38786,-25.28537 -20,-40 3.02043,-18.6127 21.14382,-40 40,-40 18.85618,0 36.97957,21.3873 40,40 2.38786,14.71463 -20,40 -20,40 0,0 12.56519,-3.0796 20,0 7.43481,3.0796 9.36405,0.69304 20,20 10.63595,19.30696 22.85714,101.42857 22.85714,101.42857 l -20,0 -22.85714,-81.42857 0,78.57143 c -2.85714,26.90476 18.57143,53.80952 15.71429,80.71428 l -24.28572,-2.5 -8.57143,83.21429 c -13.50649,0 -41.87328,0 -22.85714,0 l 0,-77.14286 -1.42857,78.57143 -21.42857,-2.85714 L 535,410.21935 511.42857,408.07649 c -3.33333,-26.19048 17.61905,-52.38095 14.28572,-78.57143 l 2.85714,-81.42857 -17.14286,80 -20,0 c 0,0 7.20371,-80.79574 17.14286,-100 9.93915,-19.20426 12.56519,-16.9204 20,-20 7.43481,-3.0796 20,0 20,0"
id="path4993"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csssczzccccccccccccccccczzc" />
<text
xml:space="preserve"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:20px;line-height:125%;font-family:'URW Chancery L';-inkscape-font-specification:'URW Chancery L Bold Italic';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="110"
y="549.50507"
id="text4995"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan4997"
x="110"
y="549.50507"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial">John McNeil</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="320"
y="549.50507"
id="text4999"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5001"
x="320"
y="549.50507">Anne McNeil</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="530"
y="549.50507"
id="text5003"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5005"
x="530"
y="549.50507">Helen McNeil</tspan></text>
<g
id="g5166"
transform="matrix(2,0,0,2,1117.4431,-216.22311)">
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path5079"
d="m -300,452.3622 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5088"
d="m -300,412.27292 0,8.92857 60,31.25 0,-8.92857 -60,-31.25"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,452.45149 0,-8.92857 60,-31.25 0,8.92857 -60,31.25"
id="path5090"
inkscape:connector-curvature="0" />
</g>
<g
id="g5154"
transform="matrix(2,0,0,2,1117.4431,-216.22311)">
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,523.07649 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
id="path5081"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300.16446,483.03742 0,8.92857 60,31.25 0,-8.92857 -60,-31.25"
id="path5117"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5119"
d="m -300.16446,523.21599 0,-8.92857 60,-31.25 0,8.92857 -60,31.25"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
y="483.03741"
x="-275.34302"
height="25"
width="10"
id="rect5121"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="482.91116"
x="-277.31256"
height="40"
width="13.686545"
id="rect5125"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="498.03741"
x="-300.34302"
height="10"
width="60"
id="rect5127"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="483.03741"
x="-275.34302"
height="40"
width="10"
id="rect5129"
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="499.55264"
x="-300.34302"
height="7.2220807"
width="60"
id="rect5131"
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5133"
d="m -300.12977,484.76182 59.9778,31.18846 0,5.42957 -60.23034,-31.31472 z"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#b3b3b3;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -240.40451,484.76182 -59.9778,31.18846 0,5.42957 60.23034,-31.31472 z"
id="path5135"
inkscape:connector-curvature="0" />
</g>
<g
id="g5173"
transform="matrix(2,0,0,2,1117.4431,-216.22311)">
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path5083"
d="m -300,593.25506 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text4292"
y="594.78485"
x="-289.54495"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:'Arial Black';-inkscape-font-specification:'Arial Black, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:60px;fill:#ffffff"
y="594.78485"
x="-289.54495"
id="tspan4294"
sodipodi:role="line">?</tspan></text>
</g>
<g
transform="matrix(2,0,0,2,913.15739,-216.22311)"
id="g5178">
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,452.3622 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
id="path5180"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,412.27292 0,8.92857 60,31.25 0,-8.92857 -60,-31.25"
id="path5182"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5184"
d="m -300,452.45149 0,-8.92857 60,-31.25 0,8.92857 -60,31.25"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<path
style="fill:#000080;fill-rule:evenodd;stroke:#000000;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 313.15739,829.92987 0,-80 c 0,-10 10,-20 20,-20 l 80,0 c 10,0 20,10 20,20 l 0,80 c 0,10 -10,20 -20,20 l -80,0 c -10,0 -20,-10 -20,-20 z"
id="path5188"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<g
transform="matrix(2,0,0,2,913.15739,-216.22311)"
id="g5208">
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,593.25506 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
id="path5210"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:'Arial Black';-inkscape-font-specification:'Arial Black, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="-289.54495"
y="594.78485"
id="text5212"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5214"
x="-289.54495"
y="594.78485"
style="font-size:60px;fill:#ffffff">?</tspan></text>
</g>
<path
style="fill:#000080;fill-rule:evenodd;stroke:#000000;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 104.58596,688.50129 0,-80 c 0,-10 10,-20 20,-20 l 80,0 c 10,0 20,10 20,20 l 0,80 c 0,10 -10,20 -20,20 l -80,0 c -10,0 -20,-10 -20,-20 z"
id="path5218"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 104.58596,608.32273 0,17.85714 120,62.5 0,-17.85714 -120,-62.5"
id="path5220"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5222"
d="m 104.58596,688.67987 0,-17.85714 120,-62.5 0,17.85714 -120,62.5"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g
id="g5224"
transform="matrix(2,0,0,2,704.58596,-216.22311)">
<path
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300,523.07649 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
id="path5226"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -300.16446,483.03742 0,8.92857 60,31.25 0,-8.92857 -60,-31.25"
id="path5228"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5230"
d="m -300.16446,523.21599 0,-8.92857 60,-31.25 0,8.92857 -60,31.25"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
y="483.03741"
x="-275.34302"
height="25"
width="10"
id="rect5232"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="482.91116"
x="-277.31256"
height="40"
width="13.686545"
id="rect5234"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="498.03741"
x="-300.34302"
height="10"
width="60"
id="rect5236"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="483.03741"
x="-275.34302"
height="40"
width="10"
id="rect5238"
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
y="499.55264"
x="-300.34302"
height="7.2220807"
width="60"
id="rect5240"
style="opacity:1;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5242"
d="m -300.12977,484.76182 59.9778,31.18846 0,5.42957 -60.23034,-31.31472 z"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#b3b3b3;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -240.40451,484.76182 -59.9778,31.18846 0,5.42957 60.23034,-31.31472 z"
id="path5244"
inkscape:connector-curvature="0" />
</g>
<g
id="g5246"
transform="matrix(2,0,0,2,704.58596,-216.22311)">
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path5248"
d="m -300,593.25506 0,-40 c 0,-5 5,-10 10,-10 l 40,0 c 5,0 10,5 10,10 l 0,40 c 0,5 -5,10 -10,10 l -40,0 c -5,0 -10,-5 -10,-10 z"
style="fill:#808080;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text5250"
y="594.78485"
x="-289.54495"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:20px;line-height:125%;font-family:'Arial Black';-inkscape-font-specification:'Arial Black, ';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:60px;fill:#ffffff"
y="594.78485"
x="-289.54495"
id="tspan5252"
sodipodi:role="line">?</tspan></text>
</g>
<g
transform="matrix(2,0,0,2,1040.3002,-373.36597)"
id="g5267">
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -363.73589,562.32313 0,8.92857 60,31.25 0,-8.92857 -60,-31.25"
id="path5269"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5271"
d="m -363.73589,602.5017 0,-8.92857 60,-31.25 0,8.92857 -60,31.25"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
y="562.32312"
x="-338.91446"
height="25"
width="10"
id="rect5273"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g5275"
transform="translate(-583.91446,359.96093)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5277"
width="13.686545"
height="40"
x="243.03046"
y="202.23593" />
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5279"
width="60"
height="10"
x="220"
y="217.3622" />
<rect
style="opacity:1;fill:#aa0000;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5281"
width="10"
height="40"
x="245"
y="202.3622" />
<rect
style="opacity:1;fill:#aa0000;fill-opacity:1;stroke:none;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5283"
width="60"
height="7.2220807"
x="220"
y="218.87743" />
</g>
<path
inkscape:connector-curvature="0"
id="path5285"
d="m -363.7012,564.04753 59.9778,31.18846 0,5.42957 -60.23034,-31.31472 z"
style="fill:#aa0000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#aa0000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m -303.97594,564.04753 -59.9778,31.18846 0,5.42957 60.23034,-31.31472 z"
id="path5287"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

View file

@ -1,4 +1,5 @@
(ns youyesyet.core
(ns ^{:doc "Devalopment launcher, entirely boilerplate from Luminus."}
youyesyet.core
(:require [youyesyet.handler :as handler]
[luminus.repl-server :as repl]
[luminus.http-server :as http]
@ -46,7 +47,9 @@
mount/start-with-args
:started)]
(log/info component "started"))
(.addShutdownHook (Runtime/getRuntime) (Thread. handler/destroy)))
(.addShutdownHook (Runtime/getRuntime)
(Thread. handler/destroy)))
(defn -main [& args]
(cond
@ -57,4 +60,9 @@
(System/exit 0))
:else
(start-app args)))
;; (mount/start)
;; (mount/stop)

View file

@ -1,10 +1,13 @@
(ns youyesyet.dev-middleware
(:require [ring.middleware.reload :refer [wrap-reload]]
(:require
;; [ring.middleware.reload :refer [wrap-reload]] ;; this fails with a self referential dependency, which I haven't tracked down.
[selmer.middleware :refer [wrap-error-page]]
[prone.middleware :refer [wrap-exceptions]]))
[prone.middleware :refer [wrap-exceptions]]
))
(defn wrap-dev [handler]
(-> handler
wrap-reload
wrap-error-page
wrap-exceptions))
(-> handler
;; wrap-reload
wrap-error-page
wrap-exceptions
))

View file

@ -1,7 +1,8 @@
(ns youyesyet.env
(:require [selmer.parser :as parser]
[clojure.tools.logging :as log]
[youyesyet.dev-middleware :refer [wrap-dev]]))
[youyesyet.dev-middleware :refer [wrap-dev]]
))
(def defaults
{:init
@ -11,4 +12,5 @@
:stop
(fn []
(log/info "\n-=[youyesyet has shut down successfully]=-"))
:middleware wrap-dev})
:middleware wrap-dev
})

View file

@ -1,4 +1,7 @@
{:dev true
:port 3000
;; when :nrepl-port is set the application starts the nREPL server on load
:nrepl-port 7000}
:nrepl-port 7000
:site-title "Project Hope"
:site-logo "img/ProjectHopeLogo.png"
:motd "motd.md"}

View file

@ -1,2 +1,4 @@
{:production true
:port 3000}
:port 3000
:site-title "Project Hope"
:site-logo "img/ProjectHopeLogo.png"}

View file

@ -1,72 +0,0 @@
(ns youyesyet.views.followup
(:require [reagent.core :as r]
[re-frame.core :refer [reg-sub subscribe]]
;; [re-frame-forms.core :as form]
;; [re-frame-forms.input :as input]
;; [re-com.core :refer [h-box v-box box gap single-dropdown input-text checkbox label title hyperlink-href p]]
;; [re-com.dropdown :refer [filter-choices-by-keyword single-dropdown-args-desc]]
[youyesyet.ui-utils :as ui]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.followup: followup-request 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 (eventually) provide the followup-request view.
;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#followup-request-form
(defn panel
"Generate the followup-request panel."
[]
(js/console.log (str "Rendering follow-up form"))
(let [issue @(subscribe [:issue])
issues @(subscribe [:issues])
elector @(subscribe [:elector])
address @(subscribe [:address])
form (form/make-form {:elector (:id elector)
:issue (:id issue)})]
[:div
[:h1 "Followup Request"]
(let [selected-elector-id (r/atom (:id elector))
selected-issue (r/atom (:id issue))]
[:form {}
[:p.widget
[:label {:for "elector"} "Elector"]
[single-dropdown
:id elector
:choices (:electors address)
:model selected-elector-id
:label-fn #(:name %)]]
[:p.widget
[:label {:for "issue"} "Issue"]
[single-dropdown
:id issue
:choices (map #({:id % :label %}) (keys issues))
:model issue]]
])]))

200
package-lock.json generated Normal file
View file

@ -0,0 +1,200 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"acorn": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
"integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"enhanced-resolve": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz",
"integrity": "sha1-oRXDJQS2MC6Fp2Jp16V8zdli41k=",
"requires": {
"graceful-fs": "^4.1.2",
"memory-fs": "^0.3.0",
"object-assign": "^4.0.1",
"tapable": "^0.2.3"
}
},
"errno": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"requires": {
"prr": "~1.0.1"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"memory-fs": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz",
"integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=",
"requires": {
"errno": "^0.1.3",
"readable-stream": "^2.0.1"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"tapable": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
"integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI="
},
"tern": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/tern/-/tern-0.21.0.tgz",
"integrity": "sha1-gJyHqCbhEklDmM+IlPfC0bNGTrc=",
"requires": {
"acorn": "^4.0.9",
"enhanced-resolve": "^2.2.2",
"glob": "^7.1.1",
"minimatch": "^3.0.3",
"resolve-from": "2.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

View file

@ -1,16 +1,19 @@
(defproject youyesyet "0.2.0"
(defproject youyesyet "0.1.0"
:description "Canvassing tool for referenda"
:url "https://github.com/simon-brooke/youyesyet"
:dependencies [[bouncer "1.0.1"]
:dependencies [[adl-support "0.1.0-SNAPSHOT"]
[bouncer "1.0.1"]
[ch.qos.logback/logback-classic "1.2.2"]
[clj-oauth "1.5.5"]
[cljsjs/react-leaflet "0.12.3-4"]
[cljs-ajax "0.5.8"]
[cljs-ajax "0.7.3"]
[com.cemerick/url "0.1.1"]
[compojure "1.5.2"]
[conman "0.6.3"]
[cprop "0.1.10"]
[day8.re-frame/http-fx "0.1.6"]
[korma "0.4.3"]
[lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
[luminus/ring-ttl-session "0.3.1"]
@ -23,13 +26,14 @@
[mount "0.1.11"]
[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.229" :scope "provided"]
[org.clojure/core.memoize "0.7.1"]
[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/font-awesome "4.7.0"]
[org.webjars.bower/tether "1.4.0"]
[re-frame "0.9.2"]
[re-frame "0.10.5"]
[reagent "0.6.1"]
[reagent-utils "0.2.1"]
[ring-middleware-format "0.7.2"]
@ -49,16 +53,15 @@
:main ^:skip-aot youyesyet.core
:migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")}
:plugins [[lein-cprop "1.0.1"]
[migratus-lein "0.4.2"]
[org.clojars.punkisdead/lein-cucumber "1.0.5"]
[lein-cljsbuild "1.1.4"]
[lein-uberwar "0.2.0"]
[lein-bower "0.5.1"]
:plugins [[lein-cljsbuild "1.1.4"]
[lein-codox "0.10.3"]
[lein-cprop "1.0.1"]
[lein-less "1.7.5"]
[lein-codox "0.10.3"]]
[lein-npm "0.6.2"]
[lein-uberwar "0.2.0"]
[migratus-lein "0.4.2"]
[org.clojars.punkisdead/lein-cucumber "1.0.5"]]
:bower-dependencies [[leaflet "0.7.3"]]
:cucumber-feature-paths ["test/clj/features"]
@ -66,6 +69,15 @@
:languages [:clojure :clojurescript]
:source-paths ["src/clj" "src/cljc" "src/cljs"]}
:npm {:dependencies [[datatables.net "1.10.19"]
[datatables.net-dt "1.10.19"]
[jquery "3.3.1"]
[leaflet "0.7.3"] ;; old version works, new ["1.3.1"] doesn't
[selectize "0.12.5"]
[signature_pad "2.3.2"]
[simplemde "1.11.2"]]
:root "resources/public/js/lib"}
:uberwar
{:handler youyesyet.handler/app
:init youyesyet.handler/init
@ -73,7 +85,8 @@
:name "youyesyet.war"}
:clean-targets ^{:protect false}
[:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]]
[:target-path [:cljsbuild :builds :app :compiler :output-dir]
[:cljsbuild :builds :app :compiler :output-to]]
:figwheel
{:http-server-root "public"
@ -113,10 +126,10 @@
[com.cemerick/piggieback "0.2.2-SNAPSHOT"]
[directory-naming/naming-java "0.8"]
[doo "0.1.7"]
[figwheel-sidecar "0.5.9"]]
[figwheel-sidecar "0.5.15"]]
:plugins [[com.jakemccrary/lein-test-refresh "0.18.1"]
[lein-doo "0.1.7"]
[lein-figwheel "0.5.9"]
[lein-figwheel "0.5.15"]
[org.clojure/clojurescript "1.9.495"]]
:cljsbuild
{:builds

View file

@ -1,669 +0,0 @@
--------------------------------------------------------------------------------
----
---- 20161014170335-basic-setup.up.sql: database schema for youyesyet.
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
---- as published by the Free Software Foundation; either version 2
---- of the License, or (at your option) any later version.
----
---- This program is distributed in the hope that it will be useful,
---- but WITHOUT ANY WARRANTY; without even the implied warranty of
---- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
---- GNU General Public License for more details.
----
---- You should have received a copy of the GNU General Public License
---- along with this program; if not, write to the Free Software
---- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
---- USA.
----
---- Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
----
--------------------------------------------------------------------------------
----
---- NOTE
---- 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.
----
--------------------------------------------------------------------------------
SET statement_timeout = 0;
--;;
SET lock_timeout = 0;
--;;
SET client_encoding = 'UTF8';
--;;
SET standard_conforming_strings = on;
--;;
SET check_function_bodies = false;
--;;
SET client_min_messages = warning;
--;;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--;;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
--;;
SET search_path = public, pg_catalog;
--;;
SET default_tablespace = '';
--;;
SET default_with_oids = false;
--;;
--
-- Name: addresses; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
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
);
--;;
ALTER TABLE public.addresses OWNER TO youyesyet;
--;;
--
-- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
--
CREATE SEQUENCE addresses_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--;;
ALTER TABLE public.addresses_id_seq OWNER TO youyesyet;
--;;
--
-- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
--
ALTER SEQUENCE addresses_id_seq OWNED BY addresses.id;
--;;
--
-- Name: authorities; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS authorities (
id character varying(32) NOT NULL
);
--;;
ALTER TABLE public.authorities OWNER TO youyesyet;
--;;
--
-- Name: canvassers; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
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,
introduced_by int references canvassers(id),
authorised boolean
);
--;;
ALTER TABLE public.canvassers OWNER TO youyesyet;
--;;
--
-- Name: districts; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS districts (
id integer NOT NULL,
name character varying(64) NOT NULL
);
--;;
ALTER TABLE public.districts OWNER TO youyesyet;
--;;
--
-- Name: electors; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS electors (
id integer NOT NULL,
name character varying(64) NOT NULL,
address_id integer NOT NULL,
phone character varying(16),
email character varying(128)
);
--;;
ALTER TABLE public.electors OWNER TO youyesyet;
--;;
--
-- Name: followupactions; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
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
);
--;;
ALTER TABLE public.followupactions OWNER TO youyesyet;
--
-- Name: followupactions_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
--
CREATE SEQUENCE followupactions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--;;
ALTER TABLE public.followupactions_id_seq OWNER TO youyesyet;
--;;
--
-- Name: followupactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
--
ALTER SEQUENCE followupactions_id_seq OWNED BY followupactions.id;
--;;
--
-- Name: followupmethods; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS followupmethods (
id character varying(32) NOT NULL
);
--;;
ALTER TABLE public.followupmethods OWNER TO youyesyet;
--;;
--
-- Name: followuprequests; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
insert into followupmethods values ('Telephone');
--;;
insert into followupmethods values ('eMail');
--;;
insert into followupmethods values ('Post');
--;;
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
);
--;;
ALTER TABLE public.followuprequests OWNER TO youyesyet;
--;;
--
-- Name: followuprequests_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
--
CREATE SEQUENCE followuprequests_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--;;
ALTER TABLE public.followuprequests_id_seq OWNER TO youyesyet;
--;;
--
-- Name: followuprequests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
--
ALTER SEQUENCE followuprequests_id_seq OWNED BY followuprequests.id;
--;;
--
-- Name: issueexpertise; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
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
);
--;;
ALTER TABLE public.issueexpertise OWNER TO youyesyet;
--;;
--
-- Name: issues; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS issues (
id character varying(32) NOT NULL,
url character varying(256)
);
--;;
ALTER TABLE public.issues OWNER TO youyesyet;
--;;
--
-- Name: options; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS options (
id character varying(32) NOT NULL
);
--;;
ALTER TABLE public.options OWNER TO youyesyet;
--;;
--
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
CREATE TABLE IF NOT EXISTS schema_migrations (
id bigint NOT NULL
);
--;;
ALTER TABLE public.schema_migrations OWNER TO youyesyet;
--;;
--
-- Name: visits; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
--
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
);
--;;
ALTER TABLE public.visits OWNER TO youyesyet;
--;;
--
-- Name: visits_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
--
CREATE SEQUENCE visits_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--;;
ALTER TABLE public.visits_id_seq OWNER TO youyesyet;
--;;
--
-- Name: visits_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
--
ALTER SEQUENCE visits_id_seq OWNED BY visits.id;
--;;
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY addresses ALTER COLUMN id SET DEFAULT nextval('addresses_id_seq'::regclass);
--;;
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followupactions ALTER COLUMN id SET DEFAULT nextval('followupactions_id_seq'::regclass);
--;;
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followuprequests ALTER COLUMN id SET DEFAULT nextval('followuprequests_id_seq'::regclass);
--;;
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY visits ALTER COLUMN id SET DEFAULT nextval('visits_id_seq'::regclass);
--;;
--
-- Name: addresses_address_key; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY addresses
ADD CONSTRAINT addresses_address_key UNIQUE (address);
--;;
--
-- Name: addresses_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY addresses
ADD CONSTRAINT addresses_pkey PRIMARY KEY (id);
--;;
--
-- Name: authorities_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY authorities
ADD CONSTRAINT authorities_pkey PRIMARY KEY (id);
--;;
--
-- Name: canvassers_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY canvassers
ADD CONSTRAINT canvassers_pkey PRIMARY KEY (id);
--;;
--
-- Name: districts_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY districts
ADD CONSTRAINT districts_pkey PRIMARY KEY (id);
--;;
--
-- Name: electors_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY electors
ADD CONSTRAINT electors_pkey PRIMARY KEY (id);
--;;
--
-- Name: followupactions_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY followupactions
ADD CONSTRAINT followupactions_pkey PRIMARY KEY (id);
--;;
--
-- Name: followupmethods_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY followupmethods
ADD CONSTRAINT followupmethods_pkey PRIMARY KEY (id);
--;;
--
-- Name: followuprequests_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY followuprequests
ADD CONSTRAINT followuprequests_pkey PRIMARY KEY (id);
--;;
--
-- Name: issues_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY issues
ADD CONSTRAINT issues_pkey PRIMARY KEY (id);
--;;
--
-- Name: options_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY options
ADD CONSTRAINT options_pkey PRIMARY KEY (id);
--;;
--
-- Name: schema_migrations_id_key; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
--ALTER TABLE ONLY schema_migrations
-- ADD CONSTRAINT schema_migrations_id_key UNIQUE (id);
--;;
--
-- Name: visits_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
--
ALTER TABLE ONLY visits
ADD CONSTRAINT visits_pkey PRIMARY KEY (id);
--;;
--
-- Name: addresses_district_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY addresses
ADD CONSTRAINT addresses_district_id_fkey FOREIGN KEY (district_id) REFERENCES districts(id);
--;;
--
-- Name: canvassers_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY canvassers
ADD CONSTRAINT canvassers_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
--;;
--
-- Name: canvassers_authority_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY canvassers
ADD CONSTRAINT canvassers_authority_id_fkey FOREIGN KEY (authority_id) REFERENCES authorities(id);
--;;
--
-- Name: canvassers_elector_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY canvassers
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
--
ALTER TABLE ONLY electors
ADD CONSTRAINT electors_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
--;;
--
-- Name: followupactions_actor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followupactions
ADD CONSTRAINT followupactions_actor_fkey FOREIGN KEY (actor) REFERENCES canvassers(id);
--;;
--
-- Name: followupactions_request_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followupactions
ADD CONSTRAINT followupactions_request_id_fkey FOREIGN KEY (request_id) REFERENCES followuprequests(id);
--;;
--
-- Name: followuprequests_elector_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followuprequests
ADD CONSTRAINT followuprequests_elector_id_fkey FOREIGN KEY (elector_id) REFERENCES electors(id);
--;;
--
-- Name: followuprequests_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followuprequests
ADD CONSTRAINT followuprequests_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id);
--;;
--
-- Name: followuprequests_method_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followuprequests
ADD CONSTRAINT followuprequests_method_id_fkey FOREIGN KEY (method_id) REFERENCES followupmethods(id);
--;;
--
-- Name: followuprequests_visit_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY followuprequests
ADD CONSTRAINT followuprequests_visit_id_fkey FOREIGN KEY (visit_id) REFERENCES visits(id);
--;;
--
-- Name: issueexpertise_canvasser_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY issueexpertise
ADD CONSTRAINT issueexpertise_canvasser_id_fkey FOREIGN KEY (canvasser_id) REFERENCES canvassers(id);
--;;
--
-- Name: issueexpertise_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY issueexpertise
ADD CONSTRAINT issueexpertise_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id);
--;;
--
-- Name: issueexpertise_method_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY issueexpertise
ADD CONSTRAINT issueexpertise_method_id_fkey FOREIGN KEY (method_id) REFERENCES followupmethods(id);
--;;
--
-- Name: visits_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY visits
ADD CONSTRAINT visits_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
--;;
--
-- Name: visits_canvasser_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
--
ALTER TABLE ONLY visits
ADD CONSTRAINT visits_canvasser_id_fkey FOREIGN KEY (canvasser_id) REFERENCES canvassers(id);
--;;
--
-- Name: public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
--;;
REVOKE ALL ON SCHEMA public FROM postgres;
--;;
GRANT ALL ON SCHEMA public TO postgres;
--;;
GRANT ALL ON SCHEMA public TO PUBLIC;
--;;
--
-- PostgreSQL database dump complete
--

View file

@ -1,13 +0,0 @@
drop table teammemberships;
drop table teamorganiserships;
drop index ix_teams_name;
drop table teams;
drop table rolememberships;
drop index ix_roles_name;
drop table roles;

View file

@ -1,39 +0,0 @@
create table if not exists roles (
id serial primary key,
name varchar(64) not null
);
create unique index ix_roles_name on roles(name);
create table if not exists rolememberships (
role_id integer not null references roles(id),
canvasser_id integer not null references canvassers(id)
);
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
);
create unique index ix_teams_name on teams(name);
create table if not exists teammemberships (
team_id integer not null references teams(id),
canvasser_id integer not null references canvassers(id)
);
create table if not exists teamorganiserships (
team_id integer not null references teams(id),
canvasser_id integer not null references canvassers(id)
);
alter table roles owner to youyesyet;
alter table rolememberships owner to youyesyet;
alter table teams owner to youyesyet;
alter table teammemberships owner to youyesyet;

View file

@ -1,17 +0,0 @@
-- 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

@ -1,58 +0,0 @@
-- 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

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

@ -1,59 +0,0 @@
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

@ -1,69 +0,0 @@
--------------------------------------------------------------------------------
----
---- 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

@ -1,87 +0,0 @@
--------------------------------------------------------------------------------
----
---- 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;
--;;

9
resources/motd.md Normal file
View file

@ -0,0 +1,9 @@
## Welcome to Project Hope
### Alpha test code
This is a voter intention information system intended to be used by the 'Yes' side in the next Scottish independence referendum.
Design documentation is [here](https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md).
Although addresses in the database mostly are real, all personal data in the database is randomly generated and does not represent real people.

View file

@ -28,6 +28,12 @@ h1 {
margin-top: 0;
}
#signature-pad {
width: 300px;
border: thin solid white;
min-height: 150px;
}
/* desktops and laptops, primarily. Adapted to mouse; targets may be small */
@media all and (min-device-width: 1025px) {

View file

@ -42,7 +42,7 @@ del {
}
div.content, form, p, pre, h1, h2, h3, h4, h5 {
padding: 0.25em 5%;
padding: 0.25em 2.5%;
}
dl, menu, ol, table, ul {
@ -78,6 +78,7 @@ header {
margin-top: 0;
width:100%;
max-width: 100%;
min-height: 96px; /* yes, we don't approve of pixel values; but this is to ensure the logo fits. */
background-color: rgb(7, 57, 106);
color: white;
}
@ -103,7 +104,7 @@ input, select {
padding: 0.25em 1.25em;
}
input.action {
input.action, input.action-safe {
color: white;
background-color: rgb( 50, 109, 177);
font-size: 125%;
@ -112,6 +113,7 @@ input.action {
input.action-dangerous {
color: white;
background-color: red;
font-size: 125%;
}
input.required:after {
@ -163,7 +165,7 @@ th {
#main-container{
}
#back-link, .back-link {
.back-link {
min-width: 8em;
padding: 0.25em 1em;
background-color: gray;
@ -173,26 +175,26 @@ th {
border-bottom-right-radius: 0.5em;
}
#back-link:hover, #back-link:active, .back-link:hover, .back-link:active, {
.back-link:hover, .back-link:active, {
text-decoration: none;
background-color: rgb(160, 160, 160);
}
#back-link:hover::before, #back-link:active::before {
.back-link:hover::before, .back-link:active::before {
content: "< ";
}
#back-link-container {
.back-link-container {
float: left;
text-align: left;
}
#back-link-container, .big-link-container {
.back-link-container, .big-link-container {
font-size: 200%;
padding: 0.5em 0;
}
#back-link-container > #back-link:hover::before, #back-link-container > #back-link:active::before {
.back-link-container > .back-link:hover::before, .back-link-container > .back-link:active::before {
}
@ -238,6 +240,11 @@ th {
border-bottom: thin solid white;
}
#site-logo {
padding: 0.5em;
float: left;
}
/* but magically appears on mouseover */
#cookies:hover #more-about-cookies {
display: block;
@ -248,6 +255,10 @@ th {
border: thin solid silver;
}
.editor-toolbar {
background-color: silver;
}
.error {
width: 100%;
background-color: red;
@ -335,7 +346,7 @@ th {
/* desktops and laptops, primarily. Adapted to mouse; targets may be small */
@media all and (min-device-width: 1025px) {
#content {
width: 80%;
width: 90%;
float: right;
padding-bottom: 5em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 983 B

View file

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 511 B

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -1,6 +1,6 @@
--------------------------------------------------------------------------------
------------------------------------------------------------------------------;
----
---- 20161014170335-basic-setup.down.sql: database schema for youyesyet.
---- locality-trigger.sql: compute localities for addresses
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
@ -19,32 +19,17 @@
----
---- Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
----
--------------------------------------------------------------------------------
------------------------------------------------------------------------------;
-- intended to reverse out the database changes made in
-- 20161014170335-basic-setup.up.sql
---- See also: src/cljc/locality.cljc
drop table addresses cascade;
--;;
drop table authorities cascade;
--;;
drop table canvassers cascade;
--;;
drop table districts cascade;
--;;
drop table electors cascade;
--;;
drop table followupactions cascade;
--;;
drop table followupmethods cascade;
--;;
drop table followuprequests cascade;
--;;
drop table issueexpertise cascade;
--;;
drop table issues cascade;
--;;
drop table options cascade;
--;;
drop table visits cascade;
--;;
CREATE FUNCTION compute_locality() RETURNS trigger AS $compute_locality$
BEGIN
NEW.locality = (1000 * floor(NEW.latitude * 100)) -
floor(NEW.longitude * 100);
RETURN NEW;
END;
$compute_locality$ LANGUAGE plpgsql;
CREATE TRIGGER compute_locality BEFORE INSERT OR UPDATE ON addresses
FOR EACH ROW EXECUTE PROCEDURE compute_locality();

View file

@ -1,6 +1,6 @@
------------------------------------------------------------------------------;
----
---- youyesyet.routes.authenticated: routes and pages for authenticated users.
---- queries.sql: manually maintained queries.
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
@ -23,322 +23,40 @@
-- This file gets slurped in and converted into simple functions by the line
-- in youyesyet.db.core.clj:
-- (conman/bind-connection *db* "sql/queries.sql")
-- (conman/bind-connection *db* "sql/queries-auto.sql" "sql/queries.sql")
-- 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
-- :name get-address :? :1
-- :doc retrieve a address given the id.
SELECT * FROM addresses
WHERE id = :id
-- :name get-addresses-by-postcode
-- :name delete-address! :! :n
-- :doc delete a address given the id
DELETE FROM addresses
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
-- Note that queries generated by ADL are in the file
-- resources/sql/queries-auto.sql; they do not have to be (and should not be)
-- redefined here.
-- :name list-addresses-by-locality :? :*
-- :doc lists all existing address records in a given locality
SELECT *
FROM addresses
WHERE locality = :locality
-- :name list-open-requests :? :*
-- :doc lists all existing followuprequest records which have not been closed and which the :expert has expertise to answer.
SELECT DISTINCT request.*,
electors.name ||', '|| electors.gender AS elector_id_expanded,
addresses.address ||', '|| addresses.postcode ||', '|| visits.date AS visit_id_expanded,
request.issue_id as issue_id_expanded,
request.method_id AS method_id_expanded,
visits.date
FROM followuprequests as request,
ln_experts_issues_canvassers as expertise,
canvassers as experts,
electors,
addresses,
visits
where not exists (select * from followupactions as action
where action.request_id = request.id
and action.closed = true)
and request.elector_id = electors.id
and request.visit_id = visits.id
and visits.address_id = addresses.id
and request.issue_id = expertise.issue_id
and expertise.canvasser_id = :expert
ORDER BY visits.date desc

View file

@ -0,0 +1,65 @@
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_admin', 'Michael Thomson', 14, 4, 'Twitter');
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_analyst', 'Jack Lang', 13, 4, 'Twitter');
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_canvasser', 'Catriona Lang', 12, 4, 'Twitter');
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_editor', 'Ursula Lang', 11, 4, 'Twitter');
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_expert', 'Charlie Gourlay', 18, 5, 'Twitter');
insert into canvassers (username, fullname, elector_id, address_id, authority_id)
values ('test_organiser', 'Jude Morrison', 15, 5, 'Twitter');
insert into ln_members_roles_canvassers
values (1, (select id from canvassers where username='test_admin'));
insert into ln_members_roles_canvassers
values (2, (select id from canvassers where username='test_analyst'));
insert into ln_members_roles_canvassers
values (3, (select id from canvassers where username='test_editor'));
insert into ln_members_roles_canvassers
values (4, (select id from canvassers where username='test_organiser'));
insert into ln_members_roles_canvassers
values (5, (select id from canvassers where username='test_expert'));
insert into ln_members_roles_canvassers
values (6, (select id from canvassers where username='test_canvasser'));
insert into teams (name, district_id, latitude, longitude)
values ('Yes Stewartry', 1, 54.94, -3.94);
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_admin'));
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_analyst'));
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_editor'));
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_organiser'));
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_expert'));
insert into ln_members_teams_canvassers
values (
(select id from teams where name='Yes Stewartry'),
(select id from canvassers where username='test_canvasser'));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block big-links %}
{% endblock %}
{% block content %}
{{motd|safe}}
{% endblock %}

View file

@ -1,4 +1,13 @@
{% extends "base-authenticated.html" %}
{% extends "base.html" %}
{% block 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>{{site-title}}: {{title}}</title>
{% endblock %}
{% block whole-page %}
<div id="app">
<div class="splash-screen">
@ -24,12 +33,21 @@
</div>
{% endblock %}
{% block extra-script %}
var user = {
"username": "{{user.username}}",
"fullname": "{{user.fullname}}",
"id": {{user.id|default:-1}},
"authorised": {{user.authorised|default:false}}
};
{% endblock %}
{% block extra-tail %}
<!-- scripts and styles -->
<!-- ATTENTION \/ -->
<!-- Leaflet -->
<link rel="stylesheet" href="vendor/leaflet/dist/leaflet.css" />
<script src="vendor/leaflet/dist/leaflet.js"></script>
<!-- ATTENTION /\ -->
<!-- Leaflet -->
<link rel="stylesheet" href="js/lib/node_modules/leaflet/dist/leaflet.css" />
{% script "js/lib/node_modules/signature_pad/dist/signature_pad.min.js" %}
{% script "js/lib/node_modules/leaflet/dist/leaflet.js" %}
{% script "/js/app.js" %}
{% endblock %}

View file

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html>
<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-site.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>{{title}}</title>
</head>
<body>
{% block whole-page %}
<header>
<div id="nav">
<img id="nav-icon" src="img/threelines.png" alt="Menu"/>
<menu id="nav-menu" class="nav">
<li class=""><a href="index.html">Home</a></li>
<li class=""><a href="library.html">Library</a></li>
<li class=""><a href="register.html">Register</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>
{% if user %}
<li id="user"><a href="profile">Logged in as {{user.username}}</a></li>
{% endif %}
</menu>
</div>
<h1>
{{title}}
</h1>
</header>
<div id="main-container" class="container">
<div id="big-links">
{% block big-links %}
{% endblock %}
</div>
<div if="#content">
{% block content %}
{% endblock %}
</div>
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</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>
</html>

View file

@ -1,58 +0,0 @@
<!DOCTYPE html>
<html>
<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-site.css" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
<title>{% block title %}{% endblock %}{{title}}</title>
</head>
<body>
<header>
<div id="nav">
<img id="nav-icon" src="img/threelines.png" alt="Menu"/>
<menu id="nav-menu" class="nav">
<li class=""><a href="home">Home</a></li>
<li class=""><a href="auth">Login</a></li>
<li class=""><a href="about">About</a></li>
</menu>
</div>
<h1>
{{title}}
</h1>
</header>
<div id="main-container" class="container">
<div id="big-links">
{% block big-links %}
{% endblock %}
</div>
<div if="#content">
{% block content %}
{% endblock %}
</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>
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
</script>
</body>
</html>

View file

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<!-- head: if you want entire custom head content, override this block. -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{% style "/css/yyy-common.css" %}
{% style "/css/yyy-site.css" %}
{% style "/css/spinner.css" %}
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
{% script "/js/lib/node_modules/jquery/dist/jquery.min.js" %}
<title>{{site-title}}: {{title}}</title>
{% endblock %}
{% block extra-head %}
<!-- extra-head: put any additional markup to go into the head of your document into this block -->
<!-- e.g. script tags, link tags -->
{% endblock %}
</head>
<body>
{% block whole-page %}
<!-- whole-page: if you want an entire custom page layout, override this block. -->
{% block top %}
<header>
<div id="nav">
<img id="nav-icon" src="{{servlet-context}}/img/threelines.png" alt="Menu"/>
<menu id="nav-menu" class="nav">
<li class=""><a href="{{servlet-context}}/home">Home</a></li>
<li class=""><a href="{{servlet-context}}/library">Library</a></li>
<li class=""><a href="{{servlet-context}}/register">Register</a></li>
<li class="">{% if user %}<a href="{{servlet-context}}/logout">Logout</a>
{% else %}<a href="{{servlet-context}}/login">Login</a>{% endif %}</li>
<li class=""><a href="{{servlet-context}}/about">About</a></li>
{% if user %}
<li id="user"><a href="{{servlet-context}}/profile">Logged in as {{user.username}}</a></li>
{% endif %}
</menu>
</div>
<h1>{{title}}</h1>
{% if message|not-empty %}
<div class="message">
{{ message }}
</div>
{% endif %}
{% if error|not-empty %}
<div class="error">
{{ error }}
</div>
{% endif %}
</header>
{% endblock %}
<div id="main-container" class="container">
<div class="back-link-container">
<a href="javascript:history.back()" class="back-link">Back</a>
</div>
<div id="big-links">
{% block big-links %}
<!-- big-links: put any main navigation links into this block -->
{% endblock %}
</div>
<div id="content">
{% block content %}
<!-- content: put your main page content into this block -->
{% endblock %}
</div>
<br clear="both"/>
</div>
{% block foot %}
<!-- foot: override this block if you don't want the standard footer -->
<footer>
<div id="credits">
<div>
<img src="{{servlet-context}}/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="{{servlet-context}}/img/credits/luminus-logo.png"/>Built with <a href="http://www.luminusweb.net/">LuminusWeb</a> ||
<img height="16" width="16" alt="Clojure" src="{{servlet-context}}/img/credits/clojure-icon.gif"/> Powered by <a href="http://clojure.org">Clojure</a> ||
<img height="16" width="16" alt="GitHub" src="{{servlet-context}}/img/credits/github-logo-transparent.png"/>Find me/fork me on <a href="https://github.com/simon-brooke/youyesyet">Github</a> ||
<img height="16" width="16" alt="Free Software Foundation" src="{{servlet-context}}/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 %}
{% endblock %}
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
{% block extra-script %}
/* extra-script: put any actual javascript source text you need into this block */
{% endblock %}
</script>
{% block extra-tail %}
<!-- extra-tail: put markup here which needs to be at the very end of the body -->
<!-- e.g. some javascript script tags, some link tags -->
{% endblock %}
</body>
</html>

View file

@ -1,41 +1,15 @@
{% extends "base-unauthenticated.html" %}
{% extends "base.html" %}
{% block big-links %}
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div>
<div class="big-link-container">
<a href="library" class="big-link" id="library-link">Browse&nbsp;the&nbsp;issues</a>
</div>
{% endblock %}
{% block content %}
<h2>
We'll be in touch!
</h2>
<p>
Thank you, {{name}}. Someone will call you shortly on {{phone}} to talk to you about
{{concern}}.
</p>
<form action="call-me" method="post">
<p class="widget">
<label for="name">Your name</label>
<input type="text" id="name" name="name"/>
</p>
<p class="widget">
<label for="postcode">Your post-code</label>
<input type="text" id="postcode" name="postcode"/>
</p>
<p class="widget">
<label for="phone">Your phone number</label>
<input type="text" id="phone" name="phone"/>
</p>
<p class="widget">
<label for="concern">Your concerns</label>
<select name="concern" id="concern">
{% for concern in concerns %}
<option>{{concern.title}}</option>
{% endfor %}
</select>
</p>
<p class="widget">
<label for="submit">&nbsp;</label>
<input name="submit" id="submit" type="submit" class="action" value="Call me!"/>
</p>
</form>
{% endblock %}

View file

@ -1,14 +1,10 @@
{% extends "base-unauthenticated.html" %}
{% block big-links %}
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<p>
Use this form to request someone to phone you to discuss your concerns about independence.
</p>
<form action="call-me" method="post">
{% csrf-field %}
<p class="widget">
<label for="name">Your name</label>
<input type="text" id="name" name="name"/>
@ -25,7 +21,7 @@
<label for="concern">Your concerns</label>
<select name="concern" id="concern">
{% for concern in concerns %}
<option>{{concern.title}}</option>
<option>{{concern.id}}</option>
{% endfor %}
</select>
</p>

View file

@ -1,60 +0,0 @@
{% 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,38 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Something bad happened</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% style "/assets/bootstrap/css/bootstrap.min.css" %}
{% style "/assets/bootstrap/css/bootstrap-theme.min.css" %}
<style type="text/css">
html {
height: 100%;
min-height: 100%;
min-width: 100%;
overflow: hidden;
width: 100%;
}
html body {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
html .container-fluid {
display: table;
height: 100%;
padding: 0;
width: 100%;
}
html .row-fluid {
display: table-cell;
height: 100%;
vertical-align: middle;
}
</style>
</head>
{% extends "base.html" %}
{% block big-links %}
{% endblock %}
{% block content %}
<body>
<div class="container-fluid">
<div class="row-fluid">
@ -52,5 +21,5 @@
</div>
</div>
</div>
</body>
</html>
{% endblock %}

View file

@ -1,9 +1,15 @@
{% extends "base-unauthenticated.html" %}
{% extends "base.html" %}
{% block big-links %}
<div class="big-link-container">
<a href="supporter" class="big-link" id="yes-link">Yes</a>
</div>
<div class="big-link-container">
<a href="notyet" class="big-link" id="not-yet-link">No</a>
</div>
<div class="big-link-container">
<a href="supporter" class="big-link" id="yes-link">Yes</a>
</div>
<div class="big-link-container">
<a href="notyet" class="big-link" id="not-yet-link">No</a>
</div>
{% endblock %}
{% block content %}
<img id="site-logo" src="img/ProjectHopeLogo4.png" alt="{{site-title}}" style="float: left;" height="345" width="345"/>
{{motd|safe}}
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends "base.html" %}
<!-- File list-followuprequests-Followuprequests.html generated 2018-07-17T07:58:11.329Z by adl.to-selmer-templates.
See [Application Description Language](https://github.com/simon-brooke/adl).-->
{% block back-links %}
<div>
<div class='back-link-container'>
<a id='prev-selector' class='back-link'>
Previous
</a>
</div>
</div>
{% endblock %}
{% block big-links %}
<div>
<div class='big-link-container'>
<a id='next-selector' role='button' class='big-link'>
Next
</a>
</div>
{% ifmemberof %}
<div class='big-link-container'>
<a href='{{servlet-context}}/form-followuprequests-Followuprequest' class='big-link'>
Add a new Followuprequest
</a>
</div>
{% endifmemberof %}
</div>
{% endblock %}
{% block content %}
<form id='list-followuprequests-Followuprequests' class='list' action='{{servlet-context}}/list-followuprequests-Followuprequests' method='POST'>
{% csrf-field %}
<input id='offset' name='offset' type='hidden' value='{{params.offset|default:0}}'/>
<input id='limit' name='limit' type='hidden' value='{{params.limit|default:50}}'/>
<table caption='followuprequests'>
<thead>
<tr>
<th>
Id
</th>
<th>
Elector_id
</th>
<th>
Visit_id
</th>
<th>
Issue_id
</th>
<th>
Method_id
</th>
<th>
&nbsp;
</th>
</tr>
<tr>
<th>
<input id='id' type='text' name='id' value='{{ params.id }}'/>
</th>
<th>
<input id='elector_id' type='text' name='elector_id' value='{{ params.elector_id }}'/>
</th>
<th>
<input id='visit_id' type='text' name='visit_id' value='{{ params.visit_id }}'/>
</th>
<th>
<input id='issue_id' type='text' name='issue_id' value='{{ params.issue_id }}'/>
</th>
<th>
<input id='method_id' type='text' name='method_id' value='{{ params.method_id }}'/>
</th>
<th>
<input type='submit' id='search-widget' value='Search'/>
</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr>
<td>
{{ record.id }}
</td>
<td>
<a href='{{servlet-context}}/form-electors-Elector?id={{ record.elector_id }}'>
{{ record.elector_id_expanded }}
</a>
</td>
<td>
<a href='{{servlet-context}}/form-visits-Visit?id={{ record.visit_id }}'>
{{ record.visit_id_expanded }}
</a>
</td>
<td>
<a href='{{servlet-context}}/form-issues-Issue?id={{ record.issue_id }}'>
{{ record.issue_id_expanded }}
</a>
</td>
<td>
<a href='{{servlet-context}}/form-followupmethods-Followupmethod?id={{ record.method_id }}'>
{{ record.method_id_expanded }}
</a>
</td>
<td>
<a href='{{servlet-context}}/issue-expert/followup-request?id={{ record.id }}'>
View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}
{% block extra-script %}
var form = document.getElementById('list-followuprequests-Followuprequests');
var ow = document.getElementById('offset');
var lw = document.getElementById('limit');
form.addEventListener('submit', function() {
ow.value='0';
});
var prevSelector = document.getElementById('prev-selector');
if (prevSelector != null) {
prevSelector.addEventListener('click', function () {
if (parseInt(ow.value)===0) {
window.location = '{{servlet-context}}/admin';
} else {
ow.value=(parseInt(ow.value)-parseInt(lw.value));
console.log('Updated offset to ' + ow.value);
form.submit();
}
});
}
document.getElementById('next-selector').addEventListener('click', function () {
ow.value=(parseInt(ow.value)+parseInt(lw.value));
console.log('Updated offset to ' + ow.value);
form.submit();
});
{% endblock %}

View file

@ -0,0 +1,157 @@
{% extends "base.html" %}
<!--
Form for issue experts to handle a request from an elector.
-->
{% block extra-head %}
{% script "/js/lib/node_modules/simplemde/dist/simplemde.min.js" %}
{% style "/js/lib/node_modules/simplemde/dist/simplemde.min.css" %}
{% endblock %}
{% block content %}
<div id='content' class='edit'>
<form action='{{servlet-context}}/issue-expert/followup-action' method='POST'>
{% csrf-field %}
<input id='id' name='id' type='hidden' value='{{record.id}}'/>
<p class='widget'>
<label for='elector_id'>
Elector
</label>
{% ifmemberof canvassers teamorganisers issueexperts analysts issueeditors admin %}
<span id='elector_id' name='elector_id' class='pseudo-widget disabled'>
{{elector.name}} ({{elector.gender}})
</span>
{% else %}
<span id='elector_id' name='elector_id' class='pseudo-widget not-authorised'>
You are not permitted to view elector of followuprequests
</span>
{% endifmemberof %}
</p>
<p class='widget'>
<label for='visit'>
Visit
</label>
{% ifmemberof issueexperts analysts issueeditors admin %}
<span id='visit' name='visit' class='pseudo-widget disabled'>
by {{visit.canvasser_id_expanded}} at {{visit.date}}
</span>
{% else %}
<span id='visit_id' name='visit_id' class='pseudo-widget not-authorised'>
You are not permitted to view visit of followuprequests
</span>
{% endifmemberof %}
</p>
<p class='widget'>
<label for='issue_id'>
{{issue.id}}
</label>
{% ifmemberof issueexperts admin %}
<div id="issue-brief">
{{issue.brief|safe}}
</div>
{% else %}
<span id='issue_id' name='issue_id' class='pseudo-widget not-authorised'>
You are not permitted to view issue of followuprequests
</span>
{% endifmemberof %}
</p>
<p class='widget'>
<label for='method_id'>
{{record.method_id}}
</label>
{% ifmemberof issueexperts admin %}
<span id='method_id' name='method_id' class='pseudo-widget disabled'>
{% ifequal record.method_id "Phone" %}{{elector.phone}}{% endifequal %}
{% ifequal record.method_id "eMail" %}<a href="mailto:{{elector.email}}">{{elector.email}}</a>{% endifequal %}
</span>
{% else %}
<span id='method_id' name='method_id' class='pseudo-widget not-authorised'>
You are not permitted to view method of followuprequests
</span>
{% endifmemberof %}
</p>
{% if actions|length > 0 %}
<p>
<label for='history'>
History
</label>
<table id='history'>
<thead>
<tr>
<th>
Actor
</th>
<th>
Date
</th>
<th>
Closed?
</th>
</tr>
</thead>
<tbody>
{% for action in actions %}
<tr>
<td>{{action.actor}}</td>
<td>{{action.date}}</td>
<td>{{action.closed}}</td>
</tr>
<tr>
<td colspan="3">{{action.notes|safe}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</p>
{% endif %}
<p class='widget'>
<label for='notes'>
Your notes
</label>
{% ifmemberof admin issueexperts %}
<textarea rows='8' cols='60' id='notes' name='notes'>
</textarea>
{% endifmemberof %}
</p>
<p class='widget'>
<label for='closed'>
Has the elector's query been satisfied?
</label>
{% ifmemberof admin issueexperts %}
<input id='closed' name='closed' type='checkbox' maxlength='' size='16'/>
{% endifmemberof %}
</p>
{% ifmemberof admin issueexperts %}
<p class='widget action-safe'>
<label for='save-button' class='action-safe'>
To save this followuprequests record
</label>
<input id='save-button' name='save-button' class='action-safe' type='submit' value='Save!'/>
</p>
{% endifmemberof %}
</form>
</div>
{% endblock %}
{% block extra-tail %}
<script type='text/javascript'>
var simplemde = new SimpleMDE({
autosave: {
enabled: true,
uniqueId: "adl-generated-{{page}}",
delay: 1000,
},
indentWithTabs: true,
insertTexts: {
horizontalRule: ["", "\n\n-----\n\n"],
image: ["![](http://", ")"],
link: ["[", "](http://)"],
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
},
showIcons: ["code"], //, "table"], - sadly, markdown-clj does not support tables
spellChecker: true,
status: ["autosave", "lines", "words", "cursor"]
});
</script>
{% endblock %}

View file

@ -1,11 +1,26 @@
{% extends "base-unauthenticated.html" %}
{% extends "base.html" %}
{% block big-links %}
{% for authority in authorities %}
<div class="big-link-container">
<a href="auth?authority={{authority.id}}" class="big-link" id="{{authority.id}}-link">
<img src="img/authorities/{{authority.id}}.png" width="32" height="32" alt="{{authority.id}}"/>
{{authority.id}}
</a>
</div>
{% endfor %}
{% endblock %}
{% block content %}
<p>
We're not going to do login in the long term; we're going to use oauth.
This is a temporary login form.
</p>
<form action="auth" method="post">
<form action="login" method="post">
{% csrf-field %}
<h2>
Or use a test username and password
</h2>
<p>
We're not going to do login in the long term; we're going to use
<a href="https://oauth.net/2/">oauth</a>.
This is a temporary login form.
</p>
<p class="widget">
<label for="username">Username</label>
<input type="text" id="username" name="username"/>

View file

@ -1,4 +1,4 @@
{% extends "base-unauthenticated.html" %}
{% extends "base.html" %}
{% block big-links %}
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>

View file

@ -1,14 +1,11 @@
{% extends "base-authenticated.html" %}
{% extends "base.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>
<a href="roles/{{role.link|lower}}" class="big-link" id="big-link">{{role.name}}</a>
</div>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block big-links %}
<div class="big-link-container">
<a href="{{servlet-context}}/profile" class="big-link" id="profile-link">Your&nbsp;profile</a>
</div>
<div class="big-link-container">
<a href="{{servlet-context}}/app" class="big-link" id="app-link">App</a>
</div>
{% endblock %}

View file

@ -1,10 +1,7 @@
{% extends "base-unauthenticated.html" %}
{% extends "base.html" %}
{% block big-links %}
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div>
<div class="big-link-container">
<a href="auth" class="big-link" id="yes-link">Yes</a>
<div class="big-link-container">
<a href="login" class="big-link" id="yes-link">Yes</a>
</div>
<div class="big-link-container">
<a href="register" class="big-link" id="signup-link">No</a>

View file

View file

@ -0,0 +1,4 @@
(ns ^{:doc "Field-level authorisation. Messy."
:author "Simon Brooke"}
youyesyet.authorisation
(:require [youyesyet.env :refer [defaults]]))

View file

@ -1,28 +1,30 @@
(ns ^{:doc "Database access functions."
:author "Simon Brooke"}
(ns ^{:doc "Database access functions, mostly from Luminus template."}
youyesyet.db.core
(:require
[cheshire.core :refer [generate-string parse-string]]
[clojure.java.jdbc :as jdbc]
[conman.core :as conman]
[youyesyet.config :refer [env]]
[mount.core :refer [defstate]])
[hugsql.core :as hugsql]
[mount.core :refer [defstate]]
[youyesyet.config :refer [env]])
(:import org.postgresql.util.PGobject
java.sql.Array
clojure.lang.IPersistentMap
clojure.lang.IPersistentVector
[java.sql
BatchUpdateException
;; BatchUpdateException
Date
Timestamp
PreparedStatement]))
(defstate ^:dynamic *db*
:start (conman/connect! {:jdbc-url (env :database-url)
:driver-class-name "org.postgresql.Driver"})
:stop (conman/disconnect! *db*))
:start (conman/connect! {:jdbc-url-env (env :database-url)
:jdbc-url "jdbc:postgresql://127.0.0.1/youyesyet_dev?user=youyesyet&password=thisisnotsecure"
:driver-class-name "org.postgresql.Driver"})
:stop (conman/disconnect! *db*))
(conman/bind-connection *db* "sql/queries.sql")
(conman/bind-connection *db* "sql/queries.auto.sql" "sql/queries.sql")
(hugsql/def-sqlvec-fns "sql/queries.auto.sql")
(defn to-date [^java.sql.Date sql-date]
(-> sql-date (.getTime) (java.util.Date.)))

View file

@ -1,484 +0,0 @@
(ns ^{:doc "Korma-flavour database setup, now obsolete but retained for documentation."
:author "Simon Brooke"} youyesyet.db.schema
(:require [clojure.java.jdbc :as sql]
[korma.core :as kc]
[youyesyet.db.core :as yyydb]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.db.schema: database schema for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Note that this is the (old) Korma way of doing things;
;;; it may not play well with migrations, nor with the HugSQL way of doing things recommended
;;; in Web Development with Clojure, Second Ed. So this may be temporary 'get us started' code,
;;; which later gets thrown away. The 'create-x-table!' functions in this file may be
;;; redundant, and if they are the namespace probably needs to be renamed to 'entities'.
;;; See also resources/migrations/20161014170335-basic-setup.up.sql
(defn create-districts-table!
"Create a table to hold the electoral districts in which electors are registered.
Note that, as this app is being developed for the independence referendum in which
polling is across the whole of Scotland, this part of the design isn't fully thought
through; if later adapted to general or local elections, some breakdown or hierarchy
of polling districts into constituencies will be required."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:districts
;; it may be necessary to have a serial abstract primary key but I suspect
;; polling districts already have numbers assigned by the Electoral Commission and
;; it would be sensible to use those. TODO: check.
[:id "integer not null primary key"]
[:name "varchar(64) not null"]
;; TODO: it would make sense to hold polygon data for polling districts so we can reflect
;; them on the map, but I haven't thought through how to do that yet.
)))
(kc/defentity district
(kc/pk :id)
(kc/table :districts)
(kc/database yyydb/*db*)
(kc/entity-fields :id :name))
(defn create-addresses-table!
"Create a table to hold the addresses at which electors are registered."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:addresses
[:id "serial not null primary key"]
;; we do NOT want to hold multiple address records for the same household. When we receive
;; the electoral roll data the addresses are likely to be text fields inlined in the elector
;; record; in digesting the roll data we need to split these out and resolve them against existing
;; addresses in the table, creating a new address record only if there's no match.
[:address "varchar(256) not null unique"]
[:postcode "varchar(16)"]
[:phone "varchar(16)"]
;; the electoral district within which this address exists
[:district_id "integer references districts(id)"]
[:latitude :real]
[:longitude :real])))
(kc/defentity address
(kc/pk :id)
(kc/table :addresses)
(kc/database yyydb/*db*)
(kc/entity-fields :id :address :postcode :phone :latitude :longitude)
(kc/has-one district))
(defn create-authorities-table!
"Create a table to hold the oauth authorities against which we with authenticate canvassers."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:authorities
[:id "varchar(32) not null primary key"]
;; more stuff here when I understand more
)))
(kc/defentity authority
(kc/pk :id)
(kc/table :authorities)
(kc/database yyydb/*db*)
(kc/entity-fields :id))
(defn create-electors-table!
"Create a table to hold electors data."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:electors
;; id should be the roll number on the electoral roll, I think, but only if this is unique
;; across Scotland. Otherwise we need a separate id field. TODO: check.
[:id "integer primary key"]
[:name "varchar(64) not null"]
[:address_id "integer not null references addresses(id)" ]
[:phone "varchar(16)"]
;; we'll probably only capture email data on electors if they request a followup
;; on a particular issue by email.
[:email "varchar(128)"])))
(kc/defentity elector
(kc/pk :id)
(kc/table :electors)
(kc/database yyydb/*db*)
(kc/entity-fields :id :name :phone :email)
(kc/has-one address))
;;; Lifecycle of the canvasser record goes like this, I think:
;;; A canvasser record is created when an existing canvasser issues an invitation to a friend.
;;; The invitation takes the form of an automatically generated email with a magic token in it.
;;; At this point the record has only an email address, the introduced_by and the magic token,
;;; which is itself probably a hash of the email address. Therefore, having the username as the
;;; primary key won't work.
;;;
;;; The invited person clicks on the link in the email and completes the sign-up form, adding
;;; their full name, and their phone number. If the username they have chosen is unique, they
;;; are then sent a second email with a new magic token, possibly a hash of email address +
;;; full name. When they click on the link in this second email, their 'authorised' flag is
;;; set to 'true'.
;;;
;;; Administrators can also create canvasser records directly.aw
;;; TODO: Do we actually need a username at all? Wouldn't the email address do?
(defn create-canvassers-table!
"Create a table to hold data on canvassers (including authentication data)."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:canvassers
;; id is the username the canvasser logs in as.
[:id "serial primary key"]
[:username "varchar(32) unique"]
[:fullname "varchar(64) not null"]
;; most canvassers will be electors, we should link them:
[:elector_id "integer references electors(id) on delete no action"]
;; but some canvassers may not be electors, so we need contact details separately:
[:address_id "integer not null references addresses(id)" ]
[:phone "varchar(16)"]
[:email "varchar(128)"]
;; with which authority do we authenticate this canvasser? I do not want to hold even
;; encrypted passwords locally
[:authority_id "varchar(32) not null references authorities(id) on delete no action"]
[:introduced_by "integer references canvassers(id)"]
[:is_admin :boolean]
;; true if the canvasser is authorised to use the app; else false. This allows us to
;; block canvassers we suspect of misbehaving.
[:authorised :boolean])))
(kc/defentity canvasser
(kc/pk :id)
(kc/table :canvassers)
(kc/database yyydb/*db*)
(kc/entity-fields :id :fullname :phone :email :is_admin :authorised)
(kc/has-one elector)
(kc/has-one address)
;; (kc/has-one canvasser {:fk :introduced_by})
(kc/has-one authority))
(defn create-visits-table!
"Create a table to record visits by canvassers to addresses (including virtual visits by telephone)."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:visits
[:id "serial not null primary key"]
[:address_id "integer not null references addresses(id)"]
[:canvasser_id "integer not null references canvassers(id)"]
[:date "timestamp with time zone not null default now()"])))
(kc/defentity visit
(kc/pk :id)
(kc/table :visits)
(kc/database yyydb/*db*)
(kc/entity-fields :id :date)
(kc/has-one address)
(kc/has-one canvasser))
(defn create-options-table!
"Create a table to record options in the vote. This app is being created for the Independence
referendum, which will have just two options, 'Yes' and 'No', but it might later be adapted
for more general political canvassing."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:options
;; id is also the text of the option; e.g. 'Yes', 'No'.
[:id "varchar(32) not null primary key"]
;; To do elections you probably need party and candidate and stuff here, but
;; for the referendum it's unnecessary.
)))
(kc/defentity option
(kc/pk :id)
(kc/table :options)
(kc/database yyydb/*db*)
(kc/entity-fields :id))
(defn create-option-district-table!
"Create a table to link options to the districts in which they are relevant. This is extremely
simple for the referendum: both options are relevant to all districts. This table is essentially
'for later expansion'."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:optionsdistricts
[:option_id "varchar(32) not null references options(option)"]
[:district_id "integer not null references districts(id)"])))
;; I think we don't need an entity for optionsdistricts, because it's just a link table.
(defn create-intention-table!
"Create a table to record the intention of an elector as solicited by a canvasser during a visit.
TODO: decide whether to insert a record in this table for 'don't knows'."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:intentions
[:id "serial primary key"]
;; the elector who gave this intention
[:elector_id "integer not null references electors(id)"]
;; the option the elector said they were planning to vote for
[:option_id "varchar(32) not null references options(option)"]
[:visit_id "integer not null references visits(id)"])))
(kc/defentity intention
(kc/pk :id)
(kc/table :intentions)
(kc/database yyydb/*db*)
(kc/entity-fields :id)
(kc/has-one elector)
(kc/has-one option)
(kc/has-one visit))
(defn create-issues-table!
"A table for issues we predict electors may raise on the doorstep, for which we may be
able to provide extra information or arrange for issue-specialists to phone and talk
to the elector."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:issues
;; short name of this issue, e.g. 'currency', 'defence', 'pensions'
[:id "varchar(32) not null primary key"]
;; URL of some brief material the canvasser can use on the doorstap
[:url "varchar(256)"])))
(kc/defentity issue
(kc/pk :id)
(kc/table :issues)
(kc/database yyydb/*db*)
(kc/entity-fields :id :url))
(defn create-followup-methods-table!
"Create a table to hold reference data on followup methods."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followupmethods
[;; the method, e.g. 'telephone', 'email', 'post'
:id "varchar(32) not null primary key"])))
(kc/defentity followup-method
(kc/pk :id)
(kc/table :followupmethods)
(kc/database yyydb/*db*)
(kc/entity-fields :id))
(defn create-issue-expertise-table!
"A table to record which canvassers have expertise in which issues, so that followup
requests can be directed to the right canvassers."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:issueexpertise
;; the expert canvasser
[:canvasser_id "integer not null references canvassers(id)"]
;; the issue they have expertise in
[:issue_id "varchar(32) not null references issues(id)"]
;; the method by which this expert can respond to electors on this issue
[:method_id "varchar(32) not null references followupmethods(id)"])))
(kc/defentity issue-expertise
(kc/table :issueexpertise)
(kc/database yyydb/*db*)
(kc/entity-fields :id)
(kc/has-one canvasser)
(kc/has-one issue)
(kc/has-one followup-method))
(defn create-followup-requests-table!
"Create a table to record requests for followup contacts on particular issues."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followuprequests
[:id "serial primary key"]
[:elector_id "integer not null references electors(id)"]
[:visit_id "integer not null references visits(id)"]
[:issue_id "varchar(32) not null references issues(id)"]
;; We probably need a followupmethod (telephone, email, postal) and, for telephone,
;; convenient times but I haven't thought through how to represent this or how
;; the user interface will work.
[:method_id "varchar(32) not null references followupmethods(id)"])))
(kc/defentity followup-request
(kc/table :followuprequests)
(kc/database yyydb/*db*)
(kc/entity-fields :id)
(kc/has-one elector)
(kc/has-one visit)
(kc/has-one issue)
(kc/has-one followup-method))
(defn create-followup-actions-table!
"Create a table to record actions on followup requests. Record in this table are almost
certainly created through a desktop-style interface rather than through te app, so it's
reasonable that there should be narrative fields."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followupactions
[:id "serial primary key"]
[:request_id "integer not null references followuprequests(id)"]
[:actor "integer not null references canvassers(id)"]
[:date "timestamp with time zone not null default now()"]
[:notes "text"]
;; true if this action closes the request
[:closed :boolean])))
(kc/defentity followup-action
(kc/table :followupactions)
(kc/database yyydb/*db*)
(kc/entity-fields :id :notes :date :closed)
(kc/has-one followup-request)
(kc/has-one canvasser {:fk :actor}))
(defn create-role-table!
"Create a table to record roles. I'm not even yet certain that this is strictly necessary,
but it allows us to record the fact that different users (canvassers) have different roles
in the system."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:roles
[:id "serial primary key"]
[:name "varchar(64) not null"])))
(defn create-role-membership-table!
"Create a link table to record membership of roles."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:rolememberships
[:role_id "integer not null references role(id)"]
[:canvasser_id "integer not null references canvasser(id)"])))
(kc/defentity role
(kc/table :roles)
(kc/database yyydb/*db*)
(kc/entity-fields :id :name)
(kc/many-to-many canvasser :rolememberships))
(defn create-team-table!
"Create a table to record teams."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:teams
[:id "serial primary key"]
[:name "varchar(64) not null"]
;; the electoral district within which this address exists
[:district_id "integer references districts(id)"]
;; nominal home location of this team
[:latitude :real]
[:longitude :real])))
(defn create-team-membership-table!
"Create a link table to record membership of team."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:teammemberships
[:team_id "integer not null references team(id)"]
[:canvasser_id "integer not null references canvasser(id)"])))
(kc/defentity team
(kc/table :teams)
(kc/database yyydb/*db*)
(kc/entity-fields :id :name :latitude :longitude)
(kc/has-one district)
(kc/many-to-many canvasser :teammemberships))
(defn init-db! []
"Initialised the whole database."
(create-districts-table!)
(create-addresses-table!)
(create-authorities-table!)
(create-electors-table!)
(create-canvassers-table!)
(create-visits-table!)
(create-options-table!)
(create-issues-table!)
(create-followup-methods-table!)
(create-issue-expertise-table!)
(create-followup-requests-table!)
(create-followup-actions-table!)
(create-role-table!)
(create-role-membership-table!)
(create-team-table!)
(create-team-membership-table!)
)

View file

@ -1,17 +1,24 @@
(ns ^{:doc "Handlers for starting and stopping the webapp."
:author "Simon Brooke"}
youyesyet.handler
(:require [compojure.core :refer [routes wrap-routes]]
[youyesyet.layout :refer [error-page]]
[youyesyet.routes.authenticated :refer [authenticated-routes]]
[youyesyet.routes.home :refer [home-routes]]
[youyesyet.routes.oauth :refer [oauth-routes]]
(:require [clojure.tools.logging :as log]
[compojure.core :refer [routes wrap-routes]]
[compojure.route :as route]
[youyesyet.env :refer [defaults]]
[mount.core :as mount]
[noir.session :as session]
[youyesyet.config :refer [env]]
[youyesyet.layout :refer [error-page]]
[youyesyet.middleware :as middleware]
[clojure.tools.logging :as log]
[youyesyet.config :refer [env]]))
[youyesyet.routes.home :refer [home-routes]]
[youyesyet.routes.auto :refer [auto-selmer-routes]]
[youyesyet.routes.auto-json :refer [auto-rest-routes]]
[youyesyet.routes.issue-experts :refer [issue-expert-routes]]
[youyesyet.routes.logged-in :refer [logged-in-routes]]
[youyesyet.routes.oauth :refer [oauth-routes]]
[youyesyet.routes.rest :refer [rest-routes]]
[youyesyet.routes.roles :refer [roles-routes]]
[youyesyet.routes.services :refer [service-routes]]
[youyesyet.env :refer [defaults]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -58,17 +65,40 @@
(shutdown-agents)
(log/info "youyesyet has shut down!"))
(def app-routes
(routes
(-> #'home-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
#'oauth-routes
#'authenticated-routes
(-> #'logged-in-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'roles-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'issue-expert-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'auto-rest-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'auto-selmer-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'auto-rest-routes
(wrap-routes middleware/wrap-formats))
(-> #'rest-routes
(wrap-routes middleware/wrap-formats))
(-> #'service-routes
(wrap-routes middleware/wrap-formats)) ;; TODO: and authentication, but let's not sweat the small stuff.
'oauth-routes
(route/resources "/")
(route/not-found
(:body
(error-page {:status 404
:title "page not found"})))))
:title "Page not found"
:message "The page you requested has not yet been implemented"})))))
(def app (middleware/wrap-base #'app-routes))

View file

@ -1,44 +1,96 @@
(ns^{:doc "Render web pages using Selmer tamplating markup."
:author "Simon Brooke"}
youyesyet.layout
(:require [selmer.parser :as parser]
[selmer.filters :as filters]
[markdown.core :refer [md-to-html-string]]
[ring.util.http-response :refer [content-type ok]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]))
(ns^{:doc "Render web pages using Selmer templating markup."
:author "Simon Brooke"}
youyesyet.layout
(:require [adl-support.tags :as tags]
[clojure.string :refer [lower-case]]
[clojure.tools.logging :as log]
[markdown.core :refer [md-to-html-string]]
[ring.util.http-response :refer [content-type ok]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
[selmer.parser :as parser]
[selmer.filters :as filters]
[youyesyet.config :refer [env]]
[youyesyet.db.core :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.layout: lay out Selmer-templated web pages.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:dynamic *app-context*)
(parser/set-resource-path! (clojure.java.io/resource "templates"))
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
(tags/add-tags)
(defn raw-get-user-roles [user]
"Return, as a set, the names of the roles of which this user is a member."
(if
user
(do
(log/debug (str "seeking roles for user " user))
(let [roles
(set (map #(lower-case (:name %)) (db/list-roles-by-canvasser db/*db* user)))]
(log/debug (str "found roles " roles " for user " user))
roles))))
;; role assignments change only rarely.
(def get-user-roles (memoize raw-get-user-roles))
(defn render
"renders the HTML template located relative to resources/templates"
[template & [params]]
(content-type
(ok
(parser/render-file
template
(assoc params
:page template
:csrf-token *anti-forgery-token*
:servlet-context *app-context*
:version (System/getProperty "youyesyet.version"))))
"text/html; charset=utf-8"))
"renders the HTML `template` located relative to resources/templates in
the context of this session and with these parameters."
;; TODO: I'm passing `session` through into render. The default luminus
;; setup doesn't do this, and Dmitri is an awful lot better at this stuff
;; than me so there's almost certainly a reason it doesn't.
[template session & [params]]
(let [user (:user session)]
(log/debug (str "layout/render: template: '" template "'"))
(content-type
(ok
(parser/render-file
template
(merge params
{:page template
:csrf-token *anti-forgery-token*
:user user
:user-roles (get-user-roles user)
:site-title (:site-title env)
:version (System/getProperty "youyesyet.version")})))
"text/html; charset=utf-8")))
(defn error-page
"error-details should be a map containing the following keys:
:status - error status
:title - error title (optional)
:message - detailed error message (optional)
returns a response map with the error page as the body
and the status specified by the status key"
:status - error status
:title - error title (optional)
:message - detailed error message (optional)
returns a response map with the error page as the body
and the status specified by the status key"
[error-details]
{:status (:status error-details)
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (parser/render-file "error.html" error-details)})
:body (render "error.html" {} error-details)})

View file

@ -1,17 +1,17 @@
(ns ^{:doc "Plumbing, mainly boilerplate from Luminus."
:author "Simon Brooke"}
(ns ^{:doc "Plumbing, mainly boilerplate from Luminus."}
youyesyet.middleware
(:require [youyesyet.env :refer [defaults]]
[clojure.tools.logging :as log]
[youyesyet.layout :refer [*app-context* error-page]]
(:require [clojure.tools.logging :as log]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.webjars :refer [wrap-webjars]]
[ring.middleware.defaults :refer [site-defaults wrap-defaults]]
[ring.middleware.format :refer [wrap-restful-format]]
[youyesyet.config :refer [env]]
[ring.middleware.webjars :refer [wrap-webjars]]
[ring-ttl-session.core :refer [ttl-memory-store]]
[ring.middleware.defaults :refer [site-defaults wrap-defaults]])
[youyesyet.env :refer [defaults]]
[youyesyet.config :refer [env]]
[youyesyet.layout :refer [*app-context* error-page]])
(:import [javax.servlet ServletContext]))
(defn wrap-context [handler]
(fn [request]
(binding [*app-context*
@ -27,6 +27,7 @@
(:app-context env))]
(handler request))))
(defn wrap-internal-error [handler]
(fn [req]
(try
@ -37,6 +38,7 @@
:title "Something very bad has happened!"
:message "We've dispatched a team of highly trained gnomes to take care of the problem."})))))
(defn wrap-csrf [handler]
(wrap-anti-forgery
handler
@ -45,6 +47,7 @@
{:status 403
:title "Invalid anti-forgery token"})}))
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
handler
@ -54,6 +57,7 @@
;; since they're not compatible with this middleware
((if (:websocket? request) handler wrapped) request))))
(defn wrap-base [handler]
(-> ((:middleware defaults) handler)
wrap-webjars

View file

@ -1,35 +1,94 @@
(ns youyesyet.oauth
(ns ^{:doc "Handle oauth with multiple authenticating authorities."
:author "Simon Brooke"} youyesyet.oauth
(:require [youyesyet.config :refer [env]]
[youyesyet.db.core :as db]
[oauth.client :as oauth]
[mount.core :refer [defstate]]
[clojure.tools.logging :as log]))
(defstate consumer
:start (oauth/make-consumer
(env :oauth-consumer-key)
(env :oauth-consumer-secret)
(env :request-token-uri)
(env :access-token-uri)
(env :authorize-uri)
:hmac-sha1))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.home: routes and pages for unauthenticated users.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-authorities
"Fetch the authorities from the database and return a map of them."
[_]
(reduce
merge
{}
(map
(fn [authority]
(hash-map
(:id authority)
(oauth/make-consumer
(:consumer_key authority)
(:consumer_secret authority)
(:request_token_uri authority)
(:access_token_uri authority)
(:authorize_uri authority)
:hmac-sha1)))
(db/list-authorities db/*db* {}))))
(def authority!
;; Closure to allow authorities map to be created once when the function is first
;; called. The argument `id` should be a string, the id of some authority
;; known to the database. As side-effect, the authorities map is bound in the
;; closure.
(let [authorities (atom nil)]
(fn [id]
(if
(nil? @authorities)
(do
(log/debug "Initialising authorities map")
(swap!
authorities
get-authorities)))
(let
[authority (@authorities id)]
(if authority
(do
(log/debug (str "Selected authority " id))
authority))))))
(defn oauth-callback-uri
"Generates the oauth request callback URI"
"Generates the oauth request callback URI."
[{:keys [headers]}]
(str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/twitter-callback"))
(str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/oauth-callback"))
(defn fetch-request-token
"Fetches a request token."
[request]
"Fetches a request token from the authority implied by this `request`."
([request auth]
(let [callback-uri (oauth-callback-uri request)]
(log/info "Fetching request token using callback-uri" callback-uri)
(oauth/request-token consumer (oauth-callback-uri request))))
(oauth/request-token auth (oauth-callback-uri request))))
([request]
(fetch-request-token request (:authority (:session request)))))
(defn fetch-access-token
[request_token]
(oauth/access-token consumer request_token (:oauth_verifier request_token)))
[request_token authority]
(oauth/access-token authority request_token (:oauth_verifier request_token)))
(defn auth-redirect-uri
"Gets the URI the user should be redirected to when authenticating."
[request-token]
(str (oauth/user-approval-uri consumer request-token)))
[request-token authority]
(str (oauth/user-approval-uri authority request-token)))

View file

@ -1,11 +0,0 @@
(ns ^{:doc "Routes/pages available to administrators, only."
:author "Simon Brooke"}
youyesyet.routes.administrator
(:require [clojure.java.io :as io]
[clojure.walk :refer [keywordize-keys]]
[compojure.core :refer [defroutes GET POST]]
[noir.response :as nresponse]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.layout :as layout]
[youyesyet.db.core :as db]))

View file

@ -1,75 +0,0 @@
(ns ^{:doc "Routes/pages available to all authenticated users."
:author "Simon Brooke"}
youyesyet.routes.authenticated
(:require [clojure.java.io :as io]
[clojure.walk :refer [keywordize-keys]]
[compojure.core :refer [defroutes GET POST]]
[noir.response :as nresponse]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.layout :as layout]
[youyesyet.db.core :as db]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.authenticated: routes and pages for authenticated users.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; This code adapted from http://www.luminusweb.net/docs#accessing_the_database
(defn post?
"Return true if the argument is a ring request which is a post request"
[request]
true)
(defn canvasser-page
"Process this canvasser request, and render the canvasser page"
[request]
(let [canvasser (if
(:params request)
(let [params (:params request)]
(if (:id params)
(if (post? request)
(db/update-canvasser! params)
(db/create-canvasser! params))
(db/get-canvasser (:id params)))
))]
(layout/render
"canvasser.html"
{:title (if canvasser
(str
"Edit canvasser "
(:fullname canvasser)
" "
(:email canvasser))
"Add new canvasser")
:canvasser canvasser
:address (if (:address_id canvasser) (db/get-address (:address_id canvasser)))})))
(defn routing-page
"Render the routing page, which offers routes according to the user's roles"
[]
(layout/render "routing.html"))
(defroutes authenticated-routes
(GET "/edit-canvasser" request (canvasser-page request))
(POST "/edit-canvasser" request (canvasser-page request))
(GET "/routing" [] (routing-page)))

View file

@ -1,13 +1,19 @@
(ns ^{:doc "Routes/pages available to unauthenticated users."
:author "Simon Brooke"} youyesyet.routes.home
(:require [clojure.walk :refer [keywordize-keys]]
[noir.response :as nresponse]
(:require [adl-support.utils :refer [safe-name]]
[clojure.java.io :as io]
[clojure.string :as s]
[clojure.tools.logging :as log]
[clojure.walk :refer [keywordize-keys]]
[markdown.core :refer [md-to-html-string]]
[noir.util.route :as route]
[youyesyet.layout :as layout]
[youyesyet.db.core :as db-core]
[compojure.core :refer [defroutes GET POST]]
[ring.util.http-response :as response]
[clojure.java.io :as io]))
[youyesyet.config :refer [env]]
[youyesyet.db.core :as db-core]
[youyesyet.layout :as layout]
[youyesyet.oauth :as oauth]
[compojure.core :refer [defroutes GET POST]]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -32,12 +38,21 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn app-page []
(layout/render "app.html"))
(defn motd
[]
"Message of the day data is currently being loaded from a file in resources.
That probably isn't the final solution, but I don't currently have a final
solution"
(let [motd (io/as-file (io/resource (env :motd)))]
(if (.exists motd) (slurp motd) "")))
(defn about-page []
(layout/render "about.html"))
(layout/render "about.html" {} {:title
(str "About " (:site-title env))
:motd (md-to-html-string (motd))}))
(defn call-me-page [request]
@ -45,65 +60,76 @@
request
(do
;; do something to store it in the database
(layout/render "call-me-accepted.html" (:params request)))
(layout/render "call-me.html"
(layout/render "call-me-accepted.html" (:session request) (:params request)))
(layout/render "call-me.html" (:session request)
{:title "Please call me!"
;; TODO: Issues need to be fetched from the database
:concerns nil})))
(defn roles-page [request]
(let
[session (:session request)
username (:user session)
user (if username (db-core/get-canvasser-by-username db-core/*db* {:username username}))
roles (if user (db-core/get-roles-by-canvasser db-core/*db* {:canvasser (:id user)}))]
(cond
roles (layout/render "roles.html"
{:title (str "Welcome " (:fullname user) ", what do you want to do?")
:user user
:roles roles})
(empty? roles)(response/found "/app")
true (assoc (response/found "/login") :session (dissoc session :user))
)))
:concerns (db-core/list-issues db-core/*db* {})})))
(defn home-page []
(layout/render "home.html" {:title "You Yes Yet?"}))
(layout/render "home.html" {} {:title "You yes yet?"
:motd (md-to-html-string (motd))}))
(defn login-page
"This is very temporary. We're going to do authentication by oauth."
[request]
(let [params (keywordize-keys (:form-params request))
(let [params (keywordize-keys (:params request))
session (:session request)
username (:username params)
user (if username (db-core/get-canvasser-by-username db-core/*db* {:username username}))
password (:password params)
redirect-to (or (:redirect-to params) "roles")]
(cond
;; this is obviously, ABSURDLY, insecure. I don't want to put just-about-good-enough,
;; it-will-do-for-now security in place; instead, I want this to be test code only
;; until we have o-auth properly working.
(and user (= username password))
(assoc (response/found redirect-to) :session (assoc session :user username))
user
(layout/render "login.html" {:title (str "User " username " is unknown") :redirect-to redirect-to})
true
(layout/render "login.html" {:title "Please log in" :redirect-to redirect-to}))))
(:authority params)
(let [auth (oauth/authority! (:authority params))]
(if auth
(do
(log/info "Attempting to authorise with authority " (:authority params))
(oauth/fetch-request-token
(assoc request :session (assoc session :authority auth))
auth))
(throw (Exception. (str "No such authority: " (:authority params))))))
;; this is obviously, ABSURDLY, insecure. I don't want to put just-about-good-enough,
;; it-will-do-for-now security in place; instead, I want this to be test code only
;; until we have o-auth properly working.
(and user (= username password))
(let
[roles (layout/get-user-roles user)]
(log/info (str "Logged in user '" username "' with roles " roles))
(assoc
(response/found redirect-to)
:session
(assoc session :user user :roles roles)))
;; if we've got a username but either no user object or else
;; the password doesn't match
username
(layout/render
"login.html"
session
{:title (str "User " username " is unknown")
:redirect-to redirect-to
:warnings ["Your user name was not recognised or your password did not match"]})
;; if we've no username, just invite the user to log in
true
(layout/render
"login.html"
session
{:title "Please log in"
:redirect-to redirect-to
:authorities (db-core/list-authorities db-core/*db*)}))))
(defroutes home-routes
(GET "/" [] (home-page))
(GET "/home" [] (home-page))
(GET "/about" [] (about-page))
(GET "/roles" request (route/restricted (roles-page request)))
(GET "/app" [] (route/restricted (app-page)))
(GET "/call-me" [] (call-me-page nil))
(POST "/call-me" request (call-me-page request))
(GET "/auth" request (login-page request))
(POST "/auth" request (login-page request))
(GET "/notyet" [] (layout/render "notyet.html"
{:title "Can we persuade you?"}))
(GET "/supporter" [] (layout/render "supporter.html"
(GET "/login" request (login-page request))
(POST "/login" request (login-page request))
(GET "/notyet" [] (layout/render "notyet.html" {}
{:title "Can we persuade you?"}))
(GET "/supporter" [] (layout/render "supporter.html" {}
{:title "Have you signed up as a canvasser yet?"})))

View file

@ -0,0 +1,99 @@
(ns ^{:doc "Routes/pages available to issue experts."
:author "Simon Brooke"} youyesyet.routes.issue-experts
(:require [adl-support.core :as support]
[adl-support.utils :refer [safe-name]]
[clojure.java.io :as io]
[clojure.string :as s]
[clojure.tools.logging :as log]
[clojure.walk :refer [keywordize-keys]]
[markdown.core :refer [md-to-html-string]]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.config :refer [env]]
[youyesyet.db.core :as db]
[youyesyet.layout :as layout]
[youyesyet.oauth :as oauth]
[compojure.core :refer [defroutes GET POST]]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.home: routes and pages for issue experts.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn list-page [request]
(layout/render
"issue-expert/list.html"
(:session request)
(let [user (:user (:session request))]
{:title "Open requests"
:user user
:records (db/list-open-requests db/*db* {:expert (:id user)})})))
(defn followup-request-page [request]
(let
[params (support/massage-params
(keywordize-keys (:params request))
(keywordize-keys (:form-params request))
#{:id})
id (:id (keywordize-keys params))
record (db/get-followuprequest db/*db* {:id id})
elector (if
record
(first
(db/search-strings-electors
db/*db* {:id (:elector_id record)})))
visit (if
record
(first
(db/search-strings-visits
db/*db* {:id (:visit_id record)})))]
(layout/render
"issue-expert/request.html"
(:session request)
{:title (str "Request from " (:name elector) " at " (:date visit))
:user (:user (:session request))
:visit visit
:actions (map
;; HTML-ise the notes in each action record
#(merge % {:notes (md-to-html-string (:notes %))})
(db/list-followupactions-by-followuprequest
db/*db* {:id id}))
:record record
:elector elector
:issue (let
[raw-issue (if
record
(db/get-issue db/*db* {:id (:issue_id record)}))]
(if raw-issue
(merge
raw-issue
{:brief (md-to-html-string (:brief raw-issue))})))})))
(defroutes issue-expert-routes
(GET "/issue-expert/list" request
(route/restricted (list-page request)))
(GET "/issue-expert/followup-request" request
(route/restricted (followup-request-page request)))
(POST "/issue-expert/followup-request" request
(route/restricted (followup-request-page request))))

View file

@ -0,0 +1,127 @@
(ns ^{:doc "Routes/pages available to authenticated users."
:author "Simon Brooke"} youyesyet.routes.logged-in
(:require [adl-support.core :as support]
[adl-support.utils :refer [safe-name]]
[clojure.java.io :as io]
[clojure.string :as s]
[clojure.tools.logging :as log]
[clojure.walk :refer [keywordize-keys]]
[markdown.core :refer [md-to-html-string]]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.config :refer [env]]
[youyesyet.db.core :as db]
[youyesyet.layout :as layout]
[youyesyet.oauth :as oauth]
[compojure.core :refer [defroutes GET POST]]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.logged-in: routes and pages for authenticated users.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn app-page [request]
(layout/render "app.html"
(:session request)
{:title "Canvasser app"}))
(defn profile-page [request]
"Show the canvassers form for the current user, only."
(let [record (-> request :session :user)]
(layout/render
"auto/form-canvassers-Canvasser.html"
(:session request)
{:title (str "Profile for " (-> request :session :user :fullname))
:record record
:elector_id
(flatten
(remove
nil?
(list
(support/do-or-log-error
(db/get-elector db/*db* {:id (:elector_id record)})
:message
"Error while fetching elector record {:id (:elector_id record)}")
(support/do-or-log-error
(db/list-electors db/*db*)
:message
"Error while fetching elector list")))),
:address_id
(flatten
(remove
nil?
(list
(support/do-or-log-error
(db/get-address db/*db* {:id (:address_id record)})
:message
"Error while fetching address record {:id (:address_id record)}")
(support/do-or-log-error
(db/list-addresses db/*db*)
:message
"Error while fetching address list")))),
:authority_id
(flatten
(remove
nil?
(list
(support/do-or-log-error
(db/get-authority db/*db* {:id (:authority_id record)})
:message
"Error while fetching authority record {:id (:authority_id record)}")
(support/do-or-log-error
(db/list-authorities db/*db*)
:message
"Error while fetching authority list")))),
:roles
(flatten
(remove
nil?
(list
nil
(support/do-or-log-error
(db/list-roles db/*db*)
:message
"Error while fetching role list")))),
:expertise
(flatten
(remove
nil?
(list
nil
(support/do-or-log-error
(db/list-issues db/*db*)
:message
"Error while fetching issue list"))))})))
(defn handle-logout
[request]
(dissoc (response/found "home") :user :roles))
(defroutes logged-in-routes
(GET "/logout" request (handle-logout request))
(GET "/profile" request (route/restricted (profile-page request)))
(GET "/app" [request] (route/restricted (app-page request)))
)

View file

@ -0,0 +1,38 @@
(ns
youyesyet.routes.manual
"Manual overrides for auto-generated routes"
(:require
[noir.response :as nresponse]
[noir.util.route :as route]
[compojure.core :refer [defroutes GET POST]]
[ring.util.http-response :as response]
[clojure.java.io :as io]
[hugsql.core :as hugsql]
[youyesyet.db.core :as db]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.manual: Manual overrides for auto-generated routes.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2017 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Any of the routes defined in auto.clj can be overridden in this file. This means that
;;; new autogenerated code will not overwrite manually generated code.

View file

@ -1,28 +1,33 @@
(ns ^{:doc "OAuth authentication routes - not finished, does not work yet."
:author "Simon Brooke"} youyesyet.routes.oauth
(:require [compojure.core :refer [defroutes GET]]
(:require [clojure.tools.logging :as log]
[compojure.core :refer [defroutes GET]]
[ring.util.http-response :refer [ok found]]
[clojure.java.io :as io]
[youyesyet.oauth :as oauth]
[clojure.tools.logging :as log]))
[youyesyet.oauth :as oauth]))
(defn oauth-init
"Initiates the Twitter OAuth"
"Initiates the OAuth with the authority implied by this `request`"
[request]
(-> (oauth/fetch-request-token request)
:oauth_token
oauth/auth-redirect-uri
found))
;; (-> (oauth/fetch-request-token request)
;; :oauth_token
;; oauth/auth-redirect-uri
;; found))
(found
(oauth/auth-redirect-uri
(:oauth_token (oauth/fetch-request-token request))
(:authority (:session request)))))
(defn oauth-callback
"Handles the callback from Twitter."
"Handles the callback from the authority."
[request_token {:keys [session]}]
; oauth request was denied by user
(if (:denied request_token)
(-> (found "/")
(-> (found "/login")
(assoc :flash {:denied true}))
; fetch the request token and do anything else you wanna do if not denied.
(let [{:keys [user_id screen_name]} (oauth/fetch-access-token request_token)]
(let [{:keys [user_id screen_name]}
(oauth/fetch-access-token request_token (:authority session))]
(log/info "successfully authenticated as" user_id screen_name)
(-> (found "/")
(assoc :session

View file

@ -0,0 +1,91 @@
(ns ^{:doc "Manually maintained routes which handle data transfer to/from the canvasser app."
:author "Simon Brooke"} youyesyet.routes.rest
(:require [clojure.core.memoize :as memo]
[clojure.java.io :as io]
[clojure.walk :refer [keywordize-keys]]
[compojure.core :refer [defroutes GET POST]]
[noir.response :as nresponse]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.locality :as l]
[youyesyet.db.core :as db]
[youyesyet.utils :refer :all]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.rest: Routes which handle data transfer to/from the
;;;; canvasser app.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2017 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; See also src/clj/youyesyet/routes/auto-json.clj
(def in-get-local-data
"Local data is volatile, because hopefully canvassers are updating it as they
work. So cache for only 90 seconds."
(memo/ttl
(fn [here]
(let [neighbourhood (l/neighbouring-localities here)
addresses (flatten
(map
#(db/list-addresses-by-locality db/*db* {:locality %})
neighbourhood))]
(map
(fn [a]
(assoc a
:dwellings
(map
(fn [d]
(assoc d
:electors
(map
(fn [e]
(assoc e
:intentions
(db/list-intentions-by-elector db/*db* {:id (:id e)})))
(db/list-electors-by-dwelling db/*db* {:id (:id d)}))))
(db/list-dwellings-by-address db/*db* {:id (:id a)}))))
addresses)))
:ttl/threshold
90000))
(defn get-local-data
"Get data local to the user of the canvasser app. Expects arguments `latitude` and
`longitude`, or `locality`. Returns a block of data for that locality"
[request]
(let
[{latitude :latitude longitude :longitude locality :locality}
(keywordize-keys (:params request))
here (if locality
(coerce-to-number locality)
(l/locality
(coerce-to-number latitude)
(coerce-to-number longitude)))]
(in-get-local-data here)))
(defroutes rest-routes
(GET "/rest/get-local-data" request (route/restricted (get-local-data request)))
;; (GET "/rest/get-issues" request (route/restricted (get-issues request)))
;; (GET "/rest/set-intention" request (route/restricted (set-intention request)))
;; (GET "/rest/request-followup" request (route/restricted (request-followup request))))
)

View file

@ -0,0 +1,74 @@
(ns ^{:doc "Routes/pages available to authenticated users in specific roles."
:author "Simon Brooke"} youyesyet.routes.roles
(:require [adl-support.core :as support]
[adl-support.utils :refer [safe-name]]
[clojure.tools.logging :as log]
[clojure.walk :refer [keywordize-keys]]
[compojure.core :refer [defroutes GET POST]]
[noir.util.route :as route]
[ring.util.http-response :as response]
[youyesyet.config :refer [env]]
[youyesyet.db.core :as db-core]
[youyesyet.routes.issue-experts :as expert]
[youyesyet.layout :as layout]
[youyesyet.oauth :as oauth]
[youyesyet.routes.auto :as auto]))
(defn roles-page [request]
"Render the routing page for the roles the currently logged in user is member of."
(let
[session (:session request)
user (:user session)
roles (if
user
(db-core/list-roles-by-canvasser db-core/*db* {:id (:id user)}))]
(log/info (str "Roles routing page; user is " user "; roles are " roles))
(cond
roles (layout/render "roles.html"
(:session request)
{:title (str "Welcome " (:fullname user) ", what do you want to do?")
:user user
:roles (map #(assoc % :link (safe-name (:name %) :sql)) roles)})
(empty? roles)(response/found "/app")
true (assoc (response/found "/login") :session (dissoc session :user)))))
(defn admins-page
[request]
(layout/render
(support/resolve-template "application-index.html")
(:session request)
{:title "Administrative menu"}))
(defn analysts-page
"My expectation is that analysts will do a lot of their work through QGIS or
some other geographical information system; so there isn't a need to put
anything sophisticated here."
[request]
(layout/render
(support/resolve-template "application-index.html")
(:session request)
{:title "Administrative menu"}))
(defn canvassers-page
[request]
(layout/render "roles/canvasser.html" (:session request) {}))
(defn team-organisers-page
[request]
(layout/render "roles/team-orgenisers.html" request {}))
(defroutes roles-routes
(GET "/roles/admin" request (route/restricted (admins-page request)))
(GET "/roles/analysts" request (route/restricted (analysts-page request)))
(GET "/roles/canvassers" request (route/restricted (canvassers-page request)))
(GET "/roles/issueeditors" request (route/restricted (auto/list-issues-Issues request)))
(GET "/roles/issueexperts" request (route/restricted (expert/list-page request)))
(GET "/roles/team_organisers" request (route/restricted (auto/list-teams-Teams request)))
(GET "/roles" request (route/restricted (roles-page request))))

View file

@ -1,21 +1,28 @@
;;;; This is probably the right way to do the API, but I don't understand it.
(ns ^{:doc "REST API."
:author "Simon Brooke"} youyesyet.routes.services
(:require [clj-http.client :as client]
[ring.util.http-response :refer :all]
[compojure.api.sweet :refer :all]
[schema.core :as s]))
[ring.util.http-response :refer :all]
[schema.core :as s]
[youyesyet.db.core :as db]))
(defapi service-routes
{:swagger {:ui "/swagger-ui"
:spec "/swagger.json"
:coercion :schema
:data {:info {:version "1.0.0"
:title "Sample API"
:description "Sample Services"}}}}
(context "/api" []
:tags ["thingie"]
(GET "/electors/:address-id" []
;; (GET "/electors-by-dwelling/:dwelling-id" []
;; :return map
;; :query-params [dwelling-id :- s/Int]
;; :summary ""
;; (db/list-electors-by-dwelling db/*db* {:id dwelling-id}))
(GET "/plus" []
:return Long
@ -45,4 +52,4 @@
:return Long
:header-params [x :- Long, y :- Long]
:summary "x^y with header-parameters"
(ok (long (Math/pow x y)))))))
(ok (long (Math/pow x y))))))

View file

@ -0,0 +1,56 @@
(ns youyesyet.locality)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.locality: compute localities.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; See also resources/sql/locality-trigger.sql
(defn locality
"Compute the locality index for this `latitude`/`longitude` pair."
[latitude longitude]
(+
(* 1000 ;; left-shift the latitude component three digits
(int
(* latitude 100)))
(- ;; invert the sign of the longitude component, since
(int ;; we're interested in localities West of Greenwich.
(* longitude 100)))))
(defn neighbouring-localities
"Return this locality with the localities immediately
north east, north, north west, east, west, south west,
south and south east of it."
;; TODO: I'm not absolutely confident of my arithmetic here!
[locality]
(list
(- locality 99)
(- locality 100)
(- locality 101)
(- locality 1)
locality
(+ locality 1)
(+ locality 99)
(+ locality 100)
(+ locality 101)))
(neighbouring-localities 5482391)

View file

@ -0,0 +1,37 @@
(ns youyesyet.utils
#?(:clj (:require [clojure.tools.logging :as log])
:cljs (:require [cljs.reader :refer [read-string]])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.utils: small utility functions.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn coerce-to-number [v]
"If it is possible to do so, coerce `v` to a number"
;; TODO: this doesn't work in cljs.
(if (number? v) v
(try
(read-string (str v))
#?(:clj (catch Exception any
(log/error (str "Could not coerce '" v "' to number: " any)))
:cljs (catch js/Object any
(js/console.log (str "Could not coerce '" v "' to number: " any)))))))

View file

@ -10,13 +10,16 @@
[re-frame.core :as rf]
[secretary.core :as secretary]
[youyesyet.canvasser-app.ajax :refer [load-interceptors!]]
[youyesyet.canvasser-app.handlers]
[youyesyet.canvasser-app.gis :refer [get-current-location]]
[youyesyet.canvasser-app.handlers :as h]
[youyesyet.canvasser-app.subscriptions]
[youyesyet.canvasser-app.ui-utils :as ui]
[youyesyet.canvasser-app.views.about :as about]
[youyesyet.canvasser-app.views.building :as building]
[youyesyet.canvasser-app.views.electors :as electors]
[youyesyet.canvasser-app.views.dwelling :as dwelling]
[youyesyet.canvasser-app.views.elector :as elector]
[youyesyet.canvasser-app.views.followup :as followup]
[youyesyet.canvasser-app.views.gdpr :as gdpr]
[youyesyet.canvasser-app.views.issue :as issue]
[youyesyet.canvasser-app.views.issues :as issues]
[youyesyet.canvasser-app.views.map :as maps])
@ -54,8 +57,14 @@
(defn building-page []
(building/panel))
(defn electors-page []
(electors/panel))
(defn dwelling-page []
(dwelling/panel))
(defn elector-page []
(elector/panel))
(defn gdpr-page []
(gdpr/panel))
(defn followup-page []
(followup/panel))
@ -72,8 +81,10 @@
(def pages
{:about #'about-page
:building #'building-page
:electors #'electors-page
:dwelling #'dwelling-page
:elector #'elector-page
:followup #'followup-page
:gdpr #'gdpr-page
:issues #'issues-page
:issue #'issue-page
:map #'map-page
@ -92,10 +103,12 @@
[:header
[ui/navbar]]
(if content [content]
[:div.error (str "No content in page " :page)])
[:div.error (str "No content in page " @(rf/subscribe [:page]))])
[:footer
[:div.error {:style [:display (if error "block" "none")]} (str error)]
[:div.feedback {:style [:display (if feedback :block :none)]} (str feedback)]
[:div.error {:style [:display (if (empty? error) :none :block)]} (apply str error)]
[:div.feedback
{:style [:display (if (empty? feedback) :none :block)]}
(apply str (map #(h/feedback-messages %) (distinct feedback)))]
[:div.queue (if
(nil? outqueue) ""
(str (count outqueue) " items queued to send"))]]]))
@ -104,35 +117,61 @@
;; Routes
(secretary/set-config! :prefix "#")
(defn log-and-dispatch [arg]
(js/console.log (str "Dispatching " arg))
(rf/dispatch arg))
(secretary/defroute "/" []
(rf/dispatch [:set-active-page :map]))
(log-and-dispatch [:set-active-page :map]))
(secretary/defroute "/about" []
(rf/dispatch [:set-active-page :about]))
(log-and-dispatch [:set-active-page :about]))
(secretary/defroute "/electors/:dwelling" {dwelling-id :dwelling}
(rf/dispatch [:set-dwelling dwelling-id]))
(secretary/defroute "/dwelling" []
(log-and-dispatch [:set-active-page :dwelling]))
(secretary/defroute "/dwelling/:dwelling" {dwelling-id :dwelling}
(log-and-dispatch [:set-dwelling dwelling-id])
(log-and-dispatch [:set-active-page :dwelling]))
(secretary/defroute "/building/:address" {address-id :address}
(rf/dispatch [:set-address address-id]))
(log-and-dispatch [:set-address address-id]))
(secretary/defroute "/elector" []
(log-and-dispatch [:set-active-page :elector]))
(secretary/defroute "/elector/:elector" {elector-id :elector}
(log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :elector}]))
(secretary/defroute "/elector/:elector/:consent" {elector-id :elector consent :consent}
(log-and-dispatch [:set-consent-and-page {:elector-id elector-id :consent (and true consent) :page :elector}]))
(secretary/defroute "/elector" []
(log-and-dispatch [:set-active-page :elector]))
(secretary/defroute "/followup" []
(rf/dispatch [:set-active-page :followup]))
(log-and-dispatch [:set-active-page :followup]))
(secretary/defroute "/gdpr" []
(log-and-dispatch [:set-active-page :gdpr]))
(secretary/defroute "/gdpr/:elector" {elector-id :elector}
(log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :gdpr}]))
(secretary/defroute "/issues" []
(rf/dispatch [:set-active-page :issues]))
(log-and-dispatch [:set-active-page :issues]))
(secretary/defroute "/issues/:elector" {elector-id :elector}
(rf/dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}]))
(log-and-dispatch [:set-elector-and-page {:elector-id elector-id :page :issues}]))
(secretary/defroute "/issue/:issue" {issue :issue}
(rf/dispatch [:set-and-go-to-issue issue]))
(log-and-dispatch [:set-and-go-to-issue issue]))
(secretary/defroute "/map" []
(rf/dispatch [:set-active-page :map]))
(log-and-dispatch [:set-active-page :map]))
(secretary/defroute "/set-intention/:elector/:intention" {elector-id :elector intention :intention}
(rf/dispatch [:set-intention {:elector-id elector-id :intention intention}]))
(log-and-dispatch [:set-intention {:elector-id elector-id :intention intention}]))
;; -------------------------
;; History
@ -153,6 +192,11 @@
(defn init! []
(rf/dispatch-sync [:initialize-db])
(get-current-location)
(rf/dispatch [:fetch-locality])
(rf/dispatch [:fetch-options])
(rf/dispatch [:fetch-issues])
(load-interceptors!)
(hook-browser-navigation!)
(mount-components))

View file

@ -0,0 +1,139 @@
(ns ^{:doc "Canvasser app map stuff."
:author "Simon Brooke"}
youyesyet.canvasser-app.gis
(:require [cljs.reader :refer [read-string]]
[cemerick.url :refer (url url-encode)]
[day8.re-frame.http-fx]
[re-frame.core :refer [dispatch reg-event-db reg-event-fx subscribe]]
[ajax.core :refer [GET]]
[ajax.json :refer [json-request-format json-response-format]]
[youyesyet.canvasser-app.state :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.canvasser-app.gis: stuff to do with maps.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; map stuff. If we do this in canvasser-app.views.map we get circular
;; references, so do it here.
(defn get-current-location []
"Get the current location from the device."
(try
(if (.-geolocation js/navigator)
(.getCurrentPosition
(.-geolocation js/navigator)
(fn [position]
(js/console.log (str "Current location is: "
(.-latitude (.-coords position)) ", "
(.-longitude (.-coords position))))
(dispatch [:set-latitude (.-latitude (.-coords position))])
(dispatch [:set-longitude (.-longitude (.-coords position))])))
(js/console.log "Geolocation not available"))
(catch js/Object any
(js/console.log "Exception while trying to access location: " + any))))
(defn pin-image
"select the name of a suitable pin image for this address"
[address]
(let [intentions
(set
(remove
nil?
(map
:intention
(mapcat :electors
(:dwellings address)))))]
(case (count intentions)
0 "unknown-pin"
1 (str (name (first intentions)) "-pin")
"mixed-pin")))
(defn map-pin-click-handler
"On clicking on the pin, navigate to the electors at the address.
This way of doing it adds an antry in the browser location history,
so back links work."
[id]
(js/console.log (str "Click handler for address #" id))
(let [view @(subscribe [:view])
centre (.getCenter view)]
(dispatch [:set-zoom (.getZoom view)])
(dispatch [:set-latitude (.-lat centre)])
(dispatch [:set-longitude (.-lng centre)]))
(set! window.location.href (str "#building/" id)))
(defn add-map-pin
"Add a map-pin at this address in this map view"
[address view]
(let [lat (:latitude address)
lng (:longitude address)
pin (.icon js/L
(clj->js
{:iconAnchor [16 41]
:iconSize [32 42]
:iconUrl (str "img/map-pins/" (pin-image address) ".png")
:riseOnHover true
:shadowAnchor [16 23]
:shadowSize [57 24]
:shadowUrl "img/map-pins/shadow_pin.png"}))
marker (.marker js/L
(.latLng js/L lat lng)
(clj->js {:icon pin
:title (:address address)}))]
(.on (.addTo marker view) "click" (fn [_] (map-pin-click-handler (str (:id address)))))
marker))
(defn map-remove-pins
"Remove all pins from this map `view`. Side-effecty; liable to be problematic."
[view]
(if
view
(.eachLayer
view
(fn [layer]
(try
(if
(instance? js/L.Marker layer)
(.removeLayer view layer))
(catch js/Object any (js/console.log (str "Failed to remove pin '" layer "' from map: " any)))))))
view)
(defn refresh-map-pins
"Refresh the map pins on this map. Side-effecty; liable to be problematic."
[]
(let [view (map-remove-pins @(subscribe [:view]))
addresses @(subscribe [:addresses])]
(if
view
(do
(js/console.log (str "Adding " (count addresses) " pins"))
(doall (map #(add-map-pin % view) addresses)))
(js/console.log "View is not yet ready"))
view))

View file

@ -1,14 +1,20 @@
(ns ^{:doc "Canvasser app event handlers."
:author "Simon Brooke"}
youyesyet.canvasser-app.handlers
(:require [cljs.reader :refer [read-string]]
[re-frame.core :refer [dispatch reg-event-db]]
(:require [ajax.core :refer [GET]]
[ajax.json :refer [json-request-format json-response-format]]
[cemerick.url :refer (url url-encode)]
[cljs.reader :refer [read-string]]
[clojure.walk :refer [keywordize-keys]]
[day8.re-frame.http-fx]
[re-frame.core :refer [dispatch reg-event-db reg-event-fx subscribe]]
[youyesyet.canvasser-app.gis :refer [refresh-map-pins get-current-location]]
[youyesyet.canvasser-app.state :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.handlers: handlers for events.
;;;; youyesyet.canvasser-app.handlers: event handlers.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@ -33,7 +39,47 @@
"Return a state like this state except with the error and feedback messages
set nil"
[state]
(merge state {:error nil :feedback nil}))
(merge state {:error '() :feedback '()}))
(def source-host (assoc
(url js/window.location)
:path "/"
:query nil
:anchor nil))
(def feedback-messages
{:fetch-locality "Fetching local data."
:send-request "Request has been queued."
})
(defn add-to-feedback
"Add the value of `k` in `feedback-messages` to the feedback in this `db`."
[db k]
(assoc db :feedback (cons k (:feedback db))))
(defn remove-from-feedback
"Remove the value of `k` in `feedback-messages` to the feedback in this `db`."
[db k]
(assoc db
:feedback
(remove
#(= % k)
(:feedback db))))
(defn coerce-to-number [v]
"If it is possible to do so, coerce `v` to a number.
NOTE: I tried to do this in *cljc*, but it did not work. Leave it alone."
(if (number? v) v
(try
(read-string (str v))
(catch js/Object any
(js/console.log (str "Could not coerce '" v "' to number: " any))
v))))
(defn get-elector
@ -42,12 +88,19 @@
([elector-id state]
(get-elector elector-id state (:address state)))
([elector-id state address]
(first
(remove
nil?
(map
#(if (= elector-id (:id %)) %)
(:electors address))))))
(try
(first
(remove
nil?
(map
#(if (= (coerce-to-number elector-id) (:id %)) %)
(:electors state))))
(catch js/Object _
(str
"Failed to find id '"
elector-id
"' among '"
(:electors state) "'")))))
(reg-event-db
@ -56,6 +109,158 @@
db/default-db))
;; (reg-event-fx
;; :feedback
;; (fn [x y]
;; (js/console.log (str "Feedback event called with x = " x "; y = " y))
;; (:db x)))
;; (reg-event-fx
;; :issues
;; (fn [x y]
;; (js/console.log (str "Issues event called with x = " x "; y = " y))
;; (:db x)))
;; (reg-event-fx
;; :options
;; (fn [x y]
;; (js/console.log (str "Options event called with x = " x "; y = " y))
;; (:db x)))
;; (reg-event-fx
;; :event
;; (fn [x y]
;; (js/console.log (str "Event event called with x = " x "; y = " y))
;; (:db x)))
(reg-event-fx
:fetch-locality
(fn [{db :db} _]
(js/console.log "Fetching locality data")
;; we return a map of (side) effects
{:http-xhrio {:method :get
:uri (str source-host
"rest/get-local-data?latitude="
(:latitude db)
"&longitude="
(:longitude db))
:format (json-request-format)
:response-format (json-response-format {:keywords? true})
:on-success [:process-locality]
:on-failure [:bad-locality]}
:db (add-to-feedback db :fetch-locality)}))
(reg-event-db
:get-current-location
(fn [db _]
(js/console.log "Updating current location")
(assoc db :froboz (get-current-location))))
(reg-event-db
:process-locality
(fn
[db [_ response]]
(js/console.log "Updating locality data")
;; loop to do it again
(dispatch [:dispatch-later [{:ms 5000 :dispatch [:fetch-locality]}
{:ms 1000 :dispatch [:get-current-location]}]])
(assoc
(remove-from-feedback db :fetch-locality)
(refresh-map-pins)
:addresses (js->clj response))))
(reg-event-db
:bad-locality
(fn [db _]
;; TODO: signal something has failed? It doesn't matter very much, unless it keeps failing.
(js/console.log "Failed to fetch locality data")
;; loop to do it again
(dispatch [:dispatch-later [{:ms 60000 :dispatch [:fetch-locality]}
{:ms 1000 :dispatch [:get-current-location]}]])
(assoc
(remove-from-feedback db :fetch-locality)
:error (cons :fetch-locality (:error db)))))
(reg-event-fx
:fetch-options
(fn [{db :db} _]
(js/console.log "Fetching options")
;; we return a map of (side) effects
{:http-xhrio {:method :get
:uri (str source-host "json/auto/list-options")
:format (json-request-format)
:response-format (json-response-format {:keywords? true})
:on-success [:process-options]
:on-failure [:bad-options]}
:db (add-to-feedback db :fetch-options)}))
(reg-event-db
:process-options
(fn
[db [_ response]]
(let [options (js->clj response)]
(js/console.log (str "Updating options: " options))
(assoc
(remove-from-feedback db :fetch-options)
:options options))))
(reg-event-db
:bad-options
(fn [db _]
(js/console.log "Failed to fetch options")
(assoc
(remove-from-feedback db :fetch-options)
:error (cons :fetch-options (:error db)))))
(reg-event-fx
:fetch-issues
(fn [{db :db} _]
(js/console.log "Fetching issues")
;; we return a map of (side) effects
{:http-xhrio {:method :get
:uri (str source-host "json/auto/list-issues")
:format (json-request-format)
:response-format (json-response-format {:keywords? true})
:on-success [:process-issues]
:on-failure [:bad-issues]}
:db (add-to-feedback db :fetch-issues)}))
(reg-event-db
:process-issues
(fn
[db [_ response]]
(let [issues (reduce
merge {}
(map
#(hash-map (keyword (:id %)) %)
(js->clj response)))]
(js/console.log (str "Updating issues: " issues))
(assoc
(remove-from-feedback db :fetch-issues)
:issues issues))))
(reg-event-db
:bad-issues
(fn [db _]
(js/console.log "Failed to fetch issues")
(assoc
(remove-from-feedback db :fetch-issues)
:error (cons :fetch-issues (:error db)))))
(reg-event-db
:send-intention
(fn [db [_ args]]
@ -87,11 +292,11 @@
(:dwellings old-address))))]
(cond
(nil? old-elector)
(assoc db :error "No elector found; not setting intention")
(= intention (:intention old-elector))
(do
(js/console.log "Elector's intention hasn't changed; not setting intention")
db)
(assoc db :error (cons "No elector found; not setting intention" (:error db))
(= intention (:intention old-elector))
(do
(js/console.log "Elector's intention hasn't changed; not setting intention")
db))
true
(do
(js/console.log (str "Setting intention of elector " old-elector " to " intention))
@ -115,8 +320,7 @@
(if (and (:elector db) (:issue db) (:telephone db))
(do
(js/console.log "Sending request")
(assoc db
:feedback "Request has been queued"
(assoc (add-to-feedback db :send-request)
:outqueue (cons
{:elector-id (:id (:elector db))
:issue (:issue db)
@ -126,53 +330,72 @@
(reg-event-db
:set-active-page
(fn [db [_ page]]
(if page
(assoc (clear-messages db) :page page))))
(fn [db [_ k]]
(if k
(assoc (clear-messages db) :page k)
db)))
(reg-event-db
:set-address
(fn [db [_ address-id]]
(let [id (read-string address-id)
(let [id (coerce-to-number address-id)
address (first (remove nil? (map #(if (= id (:id %)) %) (:addresses db))))]
(if
(= (count (:dwellings address)) 1)
(assoc (clear-messages db)
:address address
:dwelling (first (:dwellings address))
:page :electors)
:electors (:electors (first (:dwellings address)))
:page :dwelling)
(assoc (clear-messages db)
:address address
:dwelling nil
:electors nil
:page :building)))))
(reg-event-db
:set-consent-and-page
(fn [db [_ args]]
(let [page (:page args)
consent (:consent args)
elector-id (coerce-to-number (:elector-id args))
elector (get-elector elector-id db)]
(js/console.log (str "Setting page to " page ", consent to " consent " for " (:name elector)))
(assoc (clear-messages db) :elector (assoc elector :consent true) :page page))))
(reg-event-db
:set-dwelling
(fn [db [_ dwelling-id]]
(let [id (read-string dwelling-id)
(let [id (coerce-to-number dwelling-id)
dwelling (first
(remove
nil?
(map
#(if (= id (:id %)) %)
(mapcat :dwellings (:addresses db)))))]
(assoc (clear-messages db) :dwelling dwelling :page :electors))))
(if dwelling
(assoc
(clear-messages db)
:dwelling dwelling
:electors (:electors dwelling)
:page :dwelling)))))
(reg-event-db
:set-and-go-to-issue
(fn [db [_ issue]]
(js/console.log (str "Setting page to :issue, issue to " issue))
(assoc (assoc (clear-messages db) :issue issue) :page :issue)))
(js/console.log (str "Setting page to :issue, issue to " issue ", issues are " (:issues db)))
(assoc (assoc (clear-messages db) :issue (keyword issue)) :page :issue)))
(reg-event-db
(reg-event-db
:set-elector-and-page
(fn [db [_ args]]
(let [page (:page args)
elector-id (read-string (:elector-id args))
elector-id (:elector-id args)
elector (get-elector elector-id db)]
(js/console.log (str "Setting page to " page ", elector to " elector))
(assoc (clear-messages db) :elector elector :page page))))
@ -181,7 +404,7 @@
(reg-event-db
:set-elector
(fn [db [_ elector-id]]
(let [elector (get-elector (read-string elector-id) db)]
(let [elector (get-elector (coerce-to-number elector-id) db)]
(js/console.log (str "Setting elector to " elector))
(assoc (clear-messages db) :elector elector))))
@ -190,19 +413,19 @@
:set-issue
(fn [db [_ issue]]
(js/console.log (str "Setting issue to " issue))
(assoc (clear-messages db) :issue issue)))
(assoc (clear-messages db) :issue (keyword issue))))
(reg-event-db
:set-latitude
(fn [db [_ issue]]
(assoc db :latitude issue)))
(fn [db [_ v]]
(assoc db :latitude (coerce-to-number v))))
(reg-event-db
:set-longitude
(fn [db [_ issue]]
(assoc db :longitude issue)))
(fn [db [_ v]]
(assoc db :longitude (coerce-to-number v))))
(reg-event-db

Some files were not shown because too many files have changed in this diff Show more