Merge branch 'feature/28' into develop

This commit is contained in:
Simon Brooke 2018-06-03 08:57:28 +01:00
commit 5b72ffac78
71 changed files with 4993 additions and 545 deletions

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

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View file

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

View file

@ -1,4 +1,4 @@
### YouYesYet: User-oriented specification
# YouYesYet: User-oriented specification
## Overview

View file

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

View file

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

View file

@ -53,14 +53,20 @@
[migratus-lein "0.4.2"]
[org.clojars.punkisdead/lein-cucumber "1.0.5"]
[lein-cljsbuild "1.1.4"]
[lein-codox "0.10.3"]
[lein-uberwar "0.2.0"]
[lein-bower "0.5.1"]
[lein-less "1.7.5"]]
[lein-less "1.7.5"]
[lein-codox "0.10.3"]]
:bower-dependencies [[leaflet "0.7.3"]]
:cucumber-feature-paths ["test/clj/features"]
:codox {:metadata {:doc "FIXME: write docs"}
:languages [:clojure :clojurescript]
:source-paths ["src/clj" "src/cljc" "src/cljs"]}
:uberwar
{:handler youyesyet.handler/app
:init youyesyet.handler/init
@ -121,7 +127,7 @@
{:app
{:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"]
:compiler
{:main "youyesyet.app"
{:main "youyesyet.canvasser-app.app"
:asset-path "/js/out"
:output-to "target/cljsbuild/public/js/app.js"
:output-dir "target/cljsbuild/public/js/out"

View file

@ -48,14 +48,14 @@ SET client_min_messages = warning;
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
-- 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';
-- COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
--;;
SET search_path = public, pg_catalog;
@ -118,15 +118,16 @@ ALTER TABLE public.authorities OWNER TO youyesyet;
--
CREATE TABLE IF NOT EXISTS canvassers (
id serial,
username character varying(32) NOT NULL,
fullname character varying(64) NOT NULL,
elector_id integer,
address_id integer NOT NULL,
phone character varying(16),
email character varying(128),
authority_id character varying(32) NOT NULL,
authorised boolean
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
);
--;;
@ -534,6 +535,8 @@ 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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,69 @@
--------------------------------------------------------------------------------
----
---- 20170721084900.up.sql: add dwellings table, to deal with flatted addresses.
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
---- as published by the Free Software Foundation; either version 2
---- of the License, or (at your option) any later version.
----
---- This program is distributed in the hope that it will be useful,
---- but WITHOUT ANY WARRANTY; without even the implied warranty of
---- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
---- GNU General Public License for more details.
----
---- You should have received a copy of the GNU General Public License
---- along with this program; if not, write to the Free Software
---- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
---- USA.
----
---- Copyright (C) 2017 Simon Brooke for Radical Independence Campaign
----
--------------------------------------------------------------------------------
----
---- NOTE
---- This file is essentially a Postgres schema dump of a database schema which was
---- created with the function initdb! in the file src/clj/youyesyet/db/schema.clj.
---- This file has then been mildly massaged to work with Migratus.
---- Either this file or src/clj/youyesyet/db/schema.clj is redundant; schema.clj
---- represents the older, Korma, way of doing things but does not readily allow
---- for migrations; this file represents the newer Migratus/HugSQL way. I'm not
---- certain which of these paths I'm going to go down.
----
--------------------------------------------------------------------------------
alter table canvassers add column address_id integer references addresses(id);
--;;
alter table electors add column address_id integer references addresses(id);
--;;
alter table visits add column address_id integer references addresses(id);
--;;
update canvassers set address_id =
(select address_id from dwellings where id = canvassers.dwelling_id);
--;;
update electors set address_id =
(select address_id from dwellings where id = electors.dwelling_id);
--;;
update visits set address_id =
(select address_id from dwellings where id = visits.dwelling_id);
--;;
alter table canvassers alter column address_id set not null;
--;;
alter table electors alter column address_id set not null;
--;;
alter table visits alter column address_id set not null;
--;;
alter table canvassers drop column dwelling_id;
--;;
alter table electors drop column dwelling_id;
--;;
alter table visits drop column dwelling_id;
--;;
drop table if exists dwellings;
--;;

View file

@ -0,0 +1,87 @@
--------------------------------------------------------------------------------
----
---- 20170721084900.up.sql: add dwellings table, to deal with flatted addresses.
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
---- as published by the Free Software Foundation; either version 2
---- of the License, or (at your option) any later version.
----
---- This program is distributed in the hope that it will be useful,
---- but WITHOUT ANY WARRANTY; without even the implied warranty of
---- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
---- GNU General Public License for more details.
----
---- You should have received a copy of the GNU General Public License
---- along with this program; if not, write to the Free Software
---- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
---- USA.
----
---- Copyright (C) 2017 Simon Brooke for Radical Independence Campaign
----
--------------------------------------------------------------------------------
----
---- NOTE
---- This file is essentially a Postgres schema dump of a database schema which was
---- created with the function initdb! in the file src/clj/youyesyet/db/schema.clj.
---- This file has then been mildly massaged to work with Migratus.
---- Either this file or src/clj/youyesyet/db/schema.clj is redundant; schema.clj
---- represents the older, Korma, way of doing things but does not readily allow
---- for migrations; this file represents the newer Migratus/HugSQL way. I'm not
---- certain which of these paths I'm going to go down.
----
--------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dwellings (
id serial NOT NULL primary key,
address_id integer NOT NULL references addresses(id),
sub_address varchar(16)
);
--;;
ALTER TABLE public.dwellings OWNER TO youyesyet;
--;;
INSERT INTO dwellings (address_id, sub_address)
SELECT DISTINCT id, '' FROM addresses;
--;;
alter table canvassers add column dwelling_id integer references dwellings(id);
--;;
alter table electors add column dwelling_id integer references dwellings(id);
--;;
alter table visits add column dwelling_id integer references dwellings(id);
--;;
update canvassers set dwelling_id =
(select id from dwellings where address_id = canvassers.address_id);
--;;
update electors set dwelling_id =
(select id from dwellings where address_id = electors.address_id);
--;;
update visits set dwelling_id =
(select id from dwellings where address_id = visits.address_id);
--;;
alter table canvassers alter column dwelling_id set not null;
--;;
alter table electors alter column dwelling_id set not null;
--;;
alter table visits alter column dwelling_id set not null;
--;;
alter table canvassers drop constraint canvassers_address_id_fkey;
--;;
alter table electors drop constraint electors_address_id_fkey;
--;;
alter table visits drop constraint visits_address_id_fkey;
--;;
alter table canvassers drop column address_id;
--;;
alter table electors drop column address_id;
--;;
alter table visits drop column address_id;
--;;

View file

@ -0,0 +1,24 @@
--------------------------------------------------------------------------------
----
---- 20180316110100intentions-and-options.down.sql: remove intentions and options
----
---- 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
----
--------------------------------------------------------------------------------
drop table intentions;

View file

@ -0,0 +1,30 @@
--------------------------------------------------------------------------------
----
---- 20180316110100intentions-and-options.up.sql: add intentions and options
----
---- 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
----
--------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS intentions (
visit_id int not null references visits(id) on delete no action,
elector_id int not null references electors(id) on delete no action,
option_id varchar(32) not null references options(id) on delete no action
);
ALTER TABLE intentions owner to youyesyet;

View file

@ -0,0 +1,3 @@
alter table electors drop column gender;
drop table genders;

View file

@ -0,0 +1,11 @@
create table genders (
id varchar(32) not null primary key
);
-- genders is reference data
insert into genders values ('Female');
insert into genders values ('Male');
insert into genders values ('Non-binary');
insert into genders values ('Unknown');
alter table electors add column gender varchar(32) references genders(id) default 'Unknown';

View file

@ -0,0 +1,10 @@
delete from options where id = 'Yes';
delete from options where id = 'No';
delete from issues where id = 'Currency';
delete from issues where id = 'Monarchy';
delete from issues where id = 'Defence';

View file

@ -0,0 +1,10 @@
insert into options values ('Yes');
insert into options values ('No');
insert into issues (id, url) values ('Currency', 'https://www.yyy.scot/wiki/issues/Currency');
insert into issues (id, url) values ('Monarchy', 'https://www.yyy.scot/wiki/issues/Monarchy');
insert into issues (id, url) values ('Defence', 'https://www.yyy.scot/wiki/issues/Defence');

View file

@ -0,0 +1,3 @@
delete from addresses where id < = 4;
delete from electors where id <= 10;

View file

@ -0,0 +1,41 @@
insert into addresses (id, address, postcode, latitude, longitude)
values (1, '13 Imaginary Terrace, IM1 3TE', 'IM1 3TE', 55.8253043, -4.2569057);
insert into addresses (id, address, postcode, latitude, longitude)
values (2, '15 Imaginary Terrace, IM1 3TE', 'IM1 3TE', 55.8252354, -4.2572778);
insert into addresses (id, address, postcode, latitude, longitude)
values (3, '17 Imaginary Terrace, IM1 3TE', 'IM1 3TE', 55.825166, -4.257026);
insert into addresses (id, address, postcode, latitude, longitude)
values (4, '19 Imaginary Terrace, IM1 3TE', 'IM1 3TE', 55.8250695, -4.2570239);
insert into electors (id, name, address_id, gender)
values (1, 'Alan Anderson', 1, 'Male');
insert into electors (id, name, address_id, gender)
values (2, 'Ann Anderson', 1, 'Female');
insert into electors (id, name, address_id, gender)
values (3, 'Alex Anderson', 1, 'Non-binary');
insert into electors (id, name, address_id)
values (4, 'Andy Anderson', 1);
insert into electors (id, name, address_id, gender)
values (5, 'Beryl Brown', 2, 'Female');
insert into electors (id, name, address_id, gender)
values (6, 'Betty Black', 2, 'Female');
insert into electors (id, name, address_id, gender)
values (7, 'Catriona Crathie', 3, 'Female');
insert into electors (id, name, address_id, gender)
values (8, 'Colin Caruthers', 3, 'Male');
insert into electors (id, name, address_id, gender)
values (9, 'Calum Crathie', 3, 'Unknown');
insert into electors (id, name, address_id, gender)
values (10, 'David Dewar', 4, 'Male');

View file

@ -0,0 +1 @@
alter table issues drop column current;

View file

@ -0,0 +1,2 @@
alter table issues add column current boolean default true;

View file

@ -0,0 +1,8 @@
alter table electors
add column address_id references addresses on delete no action;
update electors
set address_id =
(select address_id
from dwellings
where dwellings.id electors.dwelling_id);

View file

@ -0,0 +1,11 @@
CREATE TABLE dwellings
(
id INT NOT NULL PRIMARY KEY,
address_id INT NOT NULL references addresses on delete no action,
sub_address VARCHAR( 32)
);
alter table electors
add column dwelling_id int references dwellings on delete no action;
alter table electors drop column address_id;

View file

@ -46,6 +46,7 @@
#nav-menu {
margin: 0;
padding: 0;
width: 100%;
}
#nav menu li {

View file

@ -0,0 +1,836 @@
-- File queries.sql
-- autogenerated by adl.to-hugsql-queries at
-- 2018-05-26T15:03:25.295Z
-- See [Application Description Language](https://github.com/simon-brooke/adl).
-- :name create-address! :! :n
-- :doc creates a new address record
INSERT INTO addresses (address,
postcode,
phone,
district_id,
latitude,
longitude)
VALUES (:address,
:postcode,
:phone,
:district_id,
:latitude,
:longitude)
returning id
-- :name create-authority! :! :n
-- :doc creates a new authority record
INSERT INTO authorities (id)
VALUES (:id)
returning id
-- :name create-canvasser! :! :n
-- :doc creates a new canvasser record
INSERT INTO canvassers (username,
fullname,
elector_id,
address_id,
phone,
email,
authority_id,
authorised)
VALUES (:username,
:fullname,
:elector_id,
:address_id,
:phone,
:email,
:authority_id,
:authorised)
returning id
-- :name create-district! :! :n
-- :doc creates a new district record
INSERT INTO districts (name)
VALUES (:name)
returning id
-- :name create-dwelling! :! :n
-- :doc creates a new dwelling record
INSERT INTO dwellings (address_id,
sub-address)
VALUES (:address_id,
:sub-address)
returning id
-- :name create-elector! :! :n
-- :doc creates a new elector record
INSERT INTO electors (name,
dwelling_id,
phone,
email,
gender)
VALUES (:name,
:dwelling_id,
:phone,
:email,
:gender)
returning 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
-- :name create-followupmethod! :! :n
-- :doc creates a new followupmethod record
INSERT INTO followupmethods (id)
VALUES (:id)
returning id
-- :name create-followuprequest! :! :n
-- :doc creates a new followuprequest record
INSERT INTO followuprequests (elector_id,
visit_id,
issue_id,
method_id)
VALUES (:elector_id,
:visit_id,
:issue_id,
:method_id)
returning id
-- :name create-gender! :! :n
-- :doc creates a new gender record
INSERT INTO genders (id)
VALUES (:id)
returning id
-- :name create-intention! :! :n
-- :doc creates a new intention record
INSERT INTO intentions (visit_id,
elector_id,
option_id)
VALUES (:visit_id,
:elector_id,
:option_id)
returning Id
-- :name create-issue! :! :n
-- :doc creates a new issue record
INSERT INTO issues (url,
current,
id)
VALUES (:url,
:current,
:id)
returning id
-- :name create-issueexpertis! :! :n
-- :doc creates a new issueexpertis record
INSERT INTO issueexpertise (canvasser_id,
issue_id,
method_id)
VALUES (:canvasser_id,
:issue_id,
:method_id)
returning Id
-- :name create-option! :! :n
-- :doc creates a new option record
INSERT INTO options (id)
VALUES (:id)
returning id
-- :name create-role! :! :n
-- :doc creates a new role record
INSERT INTO roles (name)
VALUES (:name)
returning id
-- :name create-rolemembership! :! :n
-- :doc creates a new rolemembership record
INSERT INTO rolememberships (role_id,
canvasser_id)
VALUES (:role_id,
:canvasser_id)
returning Id
-- :name create-team! :! :n
-- :doc creates a new team record
INSERT INTO teams (name,
district_id,
latitude,
longitude)
VALUES (:name,
:district_id,
:latitude,
:longitude)
returning id
-- :name create-teammembership! :! :n
-- :doc creates a new teammembership record
INSERT INTO teammemberships (team_id,
canvasser_id)
VALUES (:team_id,
:canvasser_id)
returning Id
-- :name create-teamorganisership! :! :n
-- :doc creates a new teamorganisership record
INSERT INTO teamorganiserships (team_id,
canvasser_id)
VALUES (:team_id,
:canvasser_id)
returning Id
-- :name create-visit! :! :n
-- :doc creates a new visit record
INSERT INTO visits (address_id,
canvasser_id,
date)
VALUES (:address_id,
:canvasser_id,
:date)
returning id
-- :name delete-address! :! :n
-- :doc updates an existing address record
DELETE FROM addresses
WHERE addresses.id = :id
-- :name delete-authority! :! :n
-- :doc updates an existing authority record
DELETE FROM authorities
WHERE authorities.id = :id
-- :name delete-canvasser! :! :n
-- :doc updates an existing canvasser record
DELETE FROM canvassers
WHERE canvassers.id = :id
-- :name delete-district! :! :n
-- :doc updates an existing district record
DELETE FROM districts
WHERE districts.id = :id
-- :name delete-dwelling! :! :n
-- :doc updates an existing dwelling record
DELETE FROM dwellings
WHERE dwellings.id = :id
-- :name delete-elector! :! :n
-- :doc updates an existing elector record
DELETE FROM electors
WHERE electors.id = :id
-- :name delete-followupaction! :! :n
-- :doc updates an existing followupaction record
DELETE FROM followupactions
WHERE followupactions.id = :id
-- :name delete-followupmethod! :! :n
-- :doc updates an existing followupmethod record
DELETE FROM followupmethods
WHERE followupmethods.id = :id
-- :name delete-followuprequest! :! :n
-- :doc updates an existing followuprequest record
DELETE FROM followuprequests
WHERE followuprequests.id = :id
-- :name delete-gender! :! :n
-- :doc updates an existing gender record
DELETE FROM genders
WHERE genders.id = :id
-- :name delete-intention! :! :n
-- :doc updates an existing intention record
DELETE FROM intentions
WHERE intentions.Id = :Id
-- :name delete-issue! :! :n
-- :doc updates an existing issue record
DELETE FROM issues
WHERE issues.id = :id
-- :name delete-issueexpertis! :! :n
-- :doc updates an existing issueexpertis record
DELETE FROM issueexpertise
WHERE issueexpertise.Id = :Id
-- :name delete-option! :! :n
-- :doc updates an existing option record
DELETE FROM options
WHERE options.id = :id
-- :name delete-role! :! :n
-- :doc updates an existing role record
DELETE FROM roles
WHERE roles.id = :id
-- :name delete-rolemembership! :! :n
-- :doc updates an existing rolemembership record
DELETE FROM rolememberships
WHERE rolememberships.Id = :Id
-- :name delete-team! :! :n
-- :doc updates an existing team record
DELETE FROM teams
WHERE teams.id = :id
-- :name delete-teammembership! :! :n
-- :doc updates an existing teammembership record
DELETE FROM teammemberships
WHERE teammemberships.Id = :Id
-- :name delete-teamorganisership! :! :n
-- :doc updates an existing teamorganisership record
DELETE FROM teamorganiserships
WHERE teamorganiserships.Id = :Id
-- :name delete-visit! :! :n
-- :doc updates an existing visit record
DELETE FROM visits
WHERE visits.id = :id
-- :name get-address :? :1
-- :doc selects an existing address record
SELECT * FROM addresses
WHERE addresses.id = :id
ORDER BY addresses.address,
addresses.postcode,
addresses.id
-- :name get-authority :? :1
-- :doc selects an existing authority record
SELECT * FROM authorities
WHERE authorities.id = :id
-- :name get-canvasser :? :1
-- :doc selects an existing canvasser record
SELECT * FROM canvassers
WHERE canvassers.id = :id
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
-- :name get-district :? :1
-- :doc selects an existing district record
SELECT * FROM districts
WHERE districts.id = :id
ORDER BY districts.name,
districts.id
-- :name get-dwelling :? :1
-- :doc selects an existing dwelling record
SELECT * FROM dwellings
WHERE dwellings.id = :id
-- :name get-elector :? :1
-- :doc selects an existing elector record
SELECT * FROM electors
WHERE electors.id = :id
ORDER BY electors.name,
electors.phone,
electors.email,
electors.id
-- :name get-followupaction :? :1
-- :doc selects an existing followupaction record
SELECT * FROM followupactions
WHERE followupactions.id = :id
-- :name get-followupmethod :? :1
-- :doc selects an existing followupmethod record
SELECT * FROM followupmethods
WHERE followupmethods.id = :id
-- :name get-followuprequest :? :1
-- :doc selects an existing followuprequest record
SELECT * FROM followuprequests
WHERE followuprequests.id = :id
-- :name get-gender :? :1
-- :doc selects an existing gender record
SELECT * FROM genders
WHERE genders.id = :id
-- :name get-intention :? :1
-- :doc selects an existing intention record
SELECT * FROM intentions
WHERE intentions.Id = :Id
-- :name get-issue :? :1
-- :doc selects an existing issue record
SELECT * FROM issues
WHERE issues.id = :id
-- :name get-issueexpertis :? :1
-- :doc selects an existing issueexpertis record
SELECT * FROM issueexpertise
WHERE issueexpertise.Id = :Id
-- :name get-option :? :1
-- :doc selects an existing option record
SELECT * FROM options
WHERE options.id = :id
-- :name get-role :? :1
-- :doc selects an existing role record
SELECT * FROM roles
WHERE roles.id = :id
ORDER BY roles.name,
roles.id
-- :name get-rolemembership :? :1
-- :doc selects an existing rolemembership record
SELECT * FROM rolememberships
WHERE rolememberships.Id = :Id
-- :name get-team :? :1
-- :doc selects an existing team record
SELECT * FROM teams
WHERE teams.id = :id
ORDER BY teams.name,
teams.id
-- :name get-teammembership :? :1
-- :doc selects an existing teammembership record
SELECT * FROM teammemberships
WHERE teammemberships.Id = :Id
-- :name get-teamorganisership :? :1
-- :doc selects an existing teamorganisership record
SELECT * FROM teamorganiserships
WHERE teamorganiserships.Id = :Id
-- :name get-visit :? :1
-- :doc selects an existing visit record
SELECT * FROM visits
WHERE visits.id = :id
-- :name list-addresses :? :*
-- :doc lists all existing address records
SELECT * FROM addresses
ORDER BY addresses.address,
addresses.postcode,
addresses.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-addresses-by-district :? :*
-- :doc lists all existing address records related to a given district
SELECT *
FROM addresses
WHERE addresses.district_id = :id
ORDER BY addresses.address,
addresses.postcode,
addresses.id
-- :name list-authorities :? :*
-- :doc lists all existing authority records
SELECT * FROM authorities
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-canvassers :? :*
-- :doc lists all existing canvasser records
SELECT * FROM canvassers
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-canvassers-by-address :? :*
-- :doc lists all existing canvasser records related to a given address
SELECT *
FROM canvassers
WHERE canvassers.address_id = :id
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
-- :name list-canvassers-by-authority :? :*
-- :doc lists all existing canvasser records related to a given authority
SELECT *
FROM canvassers
WHERE canvassers.authority_id = :id
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
-- :name list-canvassers-by-elector :? :*
-- :doc lists all existing canvasser records related to a given elector
SELECT *
FROM canvassers
WHERE canvassers.elector_id = :id
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
-- :name list-districts :? :*
-- :doc lists all existing district records
SELECT * FROM districts
ORDER BY districts.name,
districts.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-dwellings :? :*
-- :doc lists all existing dwelling records
SELECT * FROM dwellings
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-dwellings-by-addres :? :*
-- :doc lists all existing dwelling records related to a given addres
SELECT *
FROM dwellings
WHERE dwellings.address_id = :id
-- :name list-electors :? :*
-- :doc lists all existing elector records
SELECT * FROM electors
ORDER BY electors.name,
electors.phone,
electors.email,
electors.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-electors-by-dwelling :? :*
-- :doc lists all existing elector records related to a given dwelling
SELECT *
FROM electors
WHERE electors.dwelling_id = :id
ORDER BY electors.name,
electors.phone,
electors.email,
electors.id
-- :name list-electors-by-gender :? :*
-- :doc lists all existing elector records related to a given gender
SELECT *
FROM electors
WHERE electors.gender = :id
ORDER BY electors.name,
electors.phone,
electors.email,
electors.id
-- :name list-followupactions :? :*
-- :doc lists all existing followupaction records
SELECT * FROM followupactions
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-followupactions-by-canvasser :? :*
-- :doc lists all existing followupaction records related to a given canvasser
SELECT *
FROM followupactions
WHERE followupactions.actor = :id
-- :name list-followupactions-by-followuprequest :? :*
-- :doc lists all existing followupaction records related to a given followuprequest
SELECT *
FROM followupactions
WHERE followupactions.request_id = :id
-- :name list-followupmethods :? :*
-- :doc lists all existing followupmethod records
SELECT * FROM followupmethods
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-followuprequests :? :*
-- :doc lists all existing followuprequest records
SELECT * FROM followuprequests
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-followuprequests-by-elector :? :*
-- :doc lists all existing followuprequest records related to a given elector
SELECT *
FROM followuprequests
WHERE followuprequests.elector_id = :id
-- :name list-followuprequests-by-followupmethod :? :*
-- :doc lists all existing followuprequest records related to a given followupmethod
SELECT *
FROM followuprequests
WHERE followuprequests.method_id = :id
-- :name list-followuprequests-by-issue :? :*
-- :doc lists all existing followuprequest records related to a given issue
SELECT *
FROM followuprequests
WHERE followuprequests.issue_id = :id
-- :name list-followuprequests-by-visit :? :*
-- :doc lists all existing followuprequest records related to a given visit
SELECT *
FROM followuprequests
WHERE followuprequests.visit_id = :id
-- :name list-genders :? :*
-- :doc lists all existing gender records
SELECT * FROM genders
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-intentions :? :*
-- :doc lists all existing intention records
SELECT * FROM intentions
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-intentions-by-elector :? :*
-- :doc lists all existing intention records related to a given elector
SELECT *
FROM intentions
WHERE intentions.elector_id = :id
-- :name list-intentions-by-option :? :*
-- :doc lists all existing intention records related to a given option
SELECT *
FROM intentions
WHERE intentions.option_id = :id
-- :name list-intentions-by-visit :? :*
-- :doc lists all existing intention records related to a given visit
SELECT *
FROM intentions
WHERE intentions.visit_id = :id
-- :name list-issueexpertise :? :*
-- :doc lists all existing issueexpertis records
SELECT * FROM issueexpertise
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-issueexpertise-by-canvasser :? :*
-- :doc lists all existing issueexpertis records related to a given canvasser
SELECT *
FROM issueexpertise
WHERE issueexpertise.canvasser_id = :id
-- :name list-issueexpertise-by-followupmethod :? :*
-- :doc lists all existing issueexpertis records related to a given followupmethod
SELECT *
FROM issueexpertise
WHERE issueexpertise.method_id = :id
-- :name list-issueexpertise-by-issue :? :*
-- :doc lists all existing issueexpertis records related to a given issue
SELECT *
FROM issueexpertise
WHERE issueexpertise.issue_id = :id
-- :name list-issues :? :*
-- :doc lists all existing issue records
SELECT * FROM issues
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-options :? :*
-- :doc lists all existing option records
SELECT * FROM options
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-rolememberships :? :*
-- :doc lists all existing rolemembership records
SELECT * FROM rolememberships
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-rolememberships-by-canvasser :? :*
-- :doc lists all existing rolemembership records related to a given canvasser
SELECT *
FROM rolememberships
WHERE rolememberships.canvasser_id = :id
-- :name list-rolememberships-by-role :? :*
-- :doc lists all existing rolemembership records related to a given role
SELECT *
FROM rolememberships
WHERE rolememberships.role_id = :id
-- :name list-roles :? :*
-- :doc lists all existing role records
SELECT * FROM roles
ORDER BY roles.name,
roles.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-teammemberships :? :*
-- :doc lists all existing teammembership records
SELECT * FROM teammemberships
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-teammemberships-by-canvasser :? :*
-- :doc lists all existing teammembership records related to a given canvasser
SELECT *
FROM teammemberships
WHERE teammemberships.canvasser_id = :id
-- :name list-teammemberships-by-team :? :*
-- :doc lists all existing teammembership records related to a given team
SELECT *
FROM teammemberships
WHERE teammemberships.team_id = :id
-- :name list-teamorganiserships :? :*
-- :doc lists all existing teamorganisership records
SELECT * FROM teamorganiserships
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-teamorganiserships-by-canvasser :? :*
-- :doc lists all existing teamorganisership records related to a given canvasser
SELECT *
FROM teamorganiserships
WHERE teamorganiserships.canvasser_id = :id
-- :name list-teamorganiserships-by-team :? :*
-- :doc lists all existing teamorganisership records related to a given team
SELECT *
FROM teamorganiserships
WHERE teamorganiserships.team_id = :id
-- :name list-teams :? :*
-- :doc lists all existing team records
SELECT * FROM teams
ORDER BY teams.name,
teams.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-teams-by-district :? :*
-- :doc lists all existing team records related to a given district
SELECT *
FROM teams
WHERE teams.district_id = :id
ORDER BY teams.name,
teams.id
-- :name list-visits :? :*
-- :doc lists all existing visit records
SELECT * FROM visits
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name list-visits-by-address :? :*
-- :doc lists all existing visit records related to a given address
SELECT *
FROM visits
WHERE visits.address_id = :id
-- :name list-visits-by-canvasser :? :*
-- :doc lists all existing visit records related to a given canvasser
SELECT *
FROM visits
WHERE visits.canvasser_id = :id
-- :name search-strings-address :? :1
-- :doc selects existing address records having any string field matching `:pattern` by substring match
SELECT * FROM addresses
WHERE
address LIKE '%:pattern%'
OR phone LIKE '%:pattern%'
ORDER BY addresses.address,
addresses.postcode,
addresses.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-canvasser :? :1
-- :doc selects existing canvasser records having any string field matching `:pattern` by substring match
SELECT * FROM canvassers
WHERE
username LIKE '%:pattern%'
OR fullname LIKE '%:pattern%'
OR phone LIKE '%:pattern%'
OR email LIKE '%:pattern%'
ORDER BY canvassers.username,
canvassers.fullname,
canvassers.email,
canvassers.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-district :? :1
-- :doc selects existing district records having any string field matching `:pattern` by substring match
SELECT * FROM districts
WHERE
name LIKE '%:pattern%'
ORDER BY districts.name,
districts.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-dwelling :? :1
-- :doc selects existing dwelling records having any string field matching `:pattern` by substring match
SELECT * FROM dwellings
WHERE
sub-address LIKE '%:pattern%'
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-elector :? :1
-- :doc selects existing elector records having any string field matching `:pattern` by substring match
SELECT * FROM electors
WHERE
name LIKE '%:pattern%'
OR phone LIKE '%:pattern%'
OR email LIKE '%:pattern%'
ORDER BY electors.name,
electors.phone,
electors.email,
electors.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-issue :? :1
-- :doc selects existing issue records having any string field matching `:pattern` by substring match
SELECT * FROM issues
WHERE
url LIKE '%:pattern%'
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-role :? :1
-- :doc selects existing role records having any string field matching `:pattern` by substring match
SELECT * FROM roles
WHERE
name LIKE '%:pattern%'
ORDER BY roles.name,
roles.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")
-- :name search-strings-team :? :1
-- :doc selects existing team records having any string field matching `:pattern` by substring match
SELECT * FROM teams
WHERE
name LIKE '%:pattern%'
ORDER BY teams.name,
teams.id
--~ (if (:offset params) "OFFSET :offset ")
--~ (if (:limit params) "LIMIT :limit" "LIMIT 100")

View file

@ -1,21 +1,344 @@
-- :name create-user! :! :n
-- :doc creates a new user record
INSERT INTO users
(id, first_name, last_name, email, pass)
VALUES (:id, :first_name, :last_name, :email, :pass)
------------------------------------------------------------------------------;
----
---- 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
----
------------------------------------------------------------------------------;
-- :name update-user! :! :n
-- :doc update an existing user record
UPDATE users
SET first_name = :first_name, last_name = :last_name, email = :email
-- 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")
-- 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-user :? :1
-- :doc retrieve a user given the id.
SELECT * FROM users
-- :name get-address :? :1
-- :doc retrieve a address given the id.
SELECT * FROM addresses
WHERE id = :id
-- :name delete-user! :! :n
-- :doc delete a user given the id
DELETE FROM users
-- :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

View file

@ -1,16 +1,5 @@
<!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-app.css" />
<link rel="stylesheet" type="text/css" href="css/spinner.css" />
<link href="https://fonts.googleapis.com/css?family=Archivo+Black|Archivo+Narrow" rel="stylesheet"/>
<title>You Yes Yet?</title>
</head>
<body>
{% extends "base-authenticated.html" %}
{% block whole-page %}
<div id="app">
<div class="splash-screen">
<div class="sk-fading-circle">
@ -33,21 +22,16 @@
You must enable JavaScript to use the You Yes Yet app.
</p>
</div>
<!-- scripts and styles -->
<!-- ATTENTION \/ -->
<!-- Leaflet -->
<link rel="stylesheet" href="vendor/leaflet/dist/leaflet.css" />
<script src="vendor/leaflet/dist/leaflet.js"></script>
<!-- ATTENTION /\ -->
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
</script>
{% script "/js/app.js" %}
</body>
</html>
{% endblock %}
{% block extra-script %}
<!-- scripts and styles -->
<!-- ATTENTION \/ -->
<!-- Leaflet -->
<link rel="stylesheet" href="vendor/leaflet/dist/leaflet.css" />
<script src="vendor/leaflet/dist/leaflet.js"></script>
<!-- ATTENTION /\ -->
{% script "/js/app.js" %}
{% endblock %}

View file

@ -3,12 +3,14 @@
<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-static.css" />
<link rel="stylesheet" type="text/css" href="css/yyy-common.css" />
<link rel="stylesheet" type="text/css" href="css/yyy-site.css" />
<link rel="stylesheet" type="text/css" href="css/spinner.css" />
<link 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"/>
@ -16,8 +18,12 @@
<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=""><a href="login.html">Login</a></li>
<li class="">{% if user %}<a href="logout.html">Logout</a>
{% else %}<a href="login.html">Login</a>{% endif %}</li>
<li class=""><a href="about.html">About</a></li>
{% if user %}
<li id="user"><a href="profile">Logged in as {{user.username}}</a></li>
{% endif %}
</menu>
</div>
@ -28,12 +34,40 @@
<div id="main-container" class="container">
<div id="big-links">
{{big-links}}
</div>
{% block big-links %}
{% endblock %}
</div>
<div if="#content">
{{content}}
{% block content %}
{% endblock %}
</div>
<div id="back-link-container">
<a href="javascript:history.back()" id="back-link">Back</a>
</div>
</div>
<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

@ -6,7 +6,7 @@
<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>{{title}}</title>
<title>{% block title %}{% endblock %}{{title}}</title>
</head>
<body>
<header>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
(ns youyesyet.db.core
(ns ^{:doc "Database access functions."
:author "Simon Brooke"}
youyesyet.db.core
(:require
[cheshire.core :refer [generate-string parse-string]]
[clojure.java.jdbc :as jdbc]
@ -15,14 +17,14 @@
Timestamp
PreparedStatement]))
(def ^:dynamic *db* {:name "java:comp/env/jdbc/EmployeeDB"})
;; (defstate ^:dynamic *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*))
;; (def ^:dynamic *db* {:name "java:comp/env/jdbc/EmployeeDB"})
(defstate ^:dynamic *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")
(defn to-date [^java.sql.Date sql-date]
(-> sql-date (.getTime) (java.util.Date.)))

View file

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

View file

@ -1,8 +1,12 @@
(ns youyesyet.handler
(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]]
[youyesyet.routes.auto-json-routes :refer [auto-rest-routes]]
[compojure.route :as route]
[youyesyet.env :refer [defaults]]
[mount.core :as mount]
@ -10,6 +14,29 @@
[clojure.tools.logging :as log]
[youyesyet.config :refer [env]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.handler: handlers for starting and stopping the webapp.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mount/defstate init-app
:start ((or (:init defaults) identity))
:stop ((or (:stop defaults) identity)))
@ -37,11 +64,16 @@
(-> #'home-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
(-> #'auto-rest-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
#'oauth-routes
#'authenticated-routes
(route/not-found
(:body
(error-page {:status 404
:title "page not found"})))))
(def app (middleware/wrap-base #'app-routes))
(def app #'app-routes)
;;(middleware/wrap-base #'app-routes))

View file

@ -1,4 +1,6 @@
(ns youyesyet.layout
(ns^{:doc "Render web pages using Selmer tamplating markup."
:author "Simon Brooke"}
youyesyet.layout
(:require [selmer.parser :as parser]
[selmer.filters :as filters]
[markdown.core :refer [md-to-html-string]]
@ -12,6 +14,7 @@
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
(defn render
"renders the HTML template located relative to resources/templates"
[template & [params]]
@ -26,6 +29,7 @@
: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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
(ns youyesyet.routes.home
(ns ^{:doc "Routes/pages available to unauthenticated users."
:author "Simon Brooke"} youyesyet.routes.home
(:require [clojure.walk :refer [keywordize-keys]]
[noir.response :as nresponse]
[noir.util.route :as route]
@ -8,12 +9,37 @@
[ring.util.http-response :as response]
[clojure.java.io :as io]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.routes.home: routes and pages for unauthenticated users.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn app-page []
(layout/render "app.html"))
(defn about-page []
(layout/render "about.html"))
(defn call-me-page [request]
(if
request
@ -25,28 +51,53 @@
;; 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))
)))
(defn home-page []
(layout/render "home.html" {:title "You Yes Yet?"}))
(defn login-page
"This is very temporary. We're going to do authentication by oauth."
[request]
(let [params (keywordize-keys (:form-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) "app")]
(if
(and (= username "test") (= password "test"))
(do
(assoc (response/found redirect-to) :session (assoc session :user username)))
(layout/render "login.html" {:title "Please log in" :redirect-to redirect-to}))))
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}))))
(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))

View file

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

View file

@ -0,0 +1,49 @@
(ns ^{:doc "Routes which handle data transfer to/from the canvasser app."
:author "Simon Brooke"} youyesyet.routes.rest
(:require [clojure.walk :refer [keywordize-keys]]
[noir.response :as nresponse]
[noir.util.route :as route]
[youyesyet.db.core :as db-core]
[compojure.core :refer [defroutes GET POST]]
[ring.util.http-response :as response]
[clojure.java.io :as io]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-local-data
"Get data local to the user of the canvasser app. Expects arguments `lat` and
`long`. Returns a block of data for that locality"
[request]
)
(defn get-issues
"Get current issues. No arguments expected."
[request])
(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

@ -1,4 +1,7 @@
(ns youyesyet.routes.services
;;;; 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]

View file

@ -1,4 +1,6 @@
(ns youyesyet.outqueue
(ns ^{:doc "Queue of messages waiting to be sent to the server."
:author "Simon Brooke"}
youyesyet.outqueue
(:require
#?(:clj [clojure.core]
:cljs [reagent.core :refer [atom]])))
@ -41,6 +43,7 @@
(reverse items)
(list items))})))
(defn add!
"Add this item to the queue."
[q item]
@ -49,7 +52,9 @@
(assoc a :items
(cons item (:items a))))))
(defn q?
(defn queue?
"True if x is a queue, else false."
[x]
(try
(let [q (deref x)
@ -61,21 +66,25 @@
#?(:clj (print (.getMessage any))
:cljs (js/console.log (str any))))))
(defn peek
"Look at the next item which could be removed from the queue."
[q]
(last (:items (deref q))))
(defn locked?
[q]
(:locked (deref q)))
(defn unlock!
([q ]
(unlock! q true))
([q value]
(swap! q (fn [a] (assoc a :locked (not (true? value)))))))
(defn lock!
[q]
(unlock! q false))
@ -86,6 +95,7 @@
[q]
(count (deref q)))
(defn take!
"Return the first item from the queue, rebind the queue to the remaining
items. If the queue is empty return nil."
@ -97,12 +107,13 @@
(assoc (assoc a :items new-queue) :v item))))
(:v (deref q)))
(defn maybe-process-next
"Apply this process, assumed to be a function of one argument, to the next
item in the queue, if the queue is not currently locked; return the value
returned by process."
[q process]
(if (and (q? q)(not (locked? q)))
(if (and (queue? q)(not (locked? q)))
(try
(lock! q)
(let [v (apply process (list (peek q)))]

View file

@ -1,3 +1,26 @@
(ns youyesyet.validation
(:require [bouncer.core :as b]
[bouncer.validators :as v]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.validation:
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

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

View file

@ -1,9 +1,11 @@
(ns youyesyet.views.building
(:require [re-frame.core :refer [reg-sub]]))
(ns ^{:doc "Canvasser app transciever for ajax packets."
:author "Simon Brooke"}
youyesyet.canvasser-app.ajax
(:require [ajax.core :as ajax]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.building: building view for youyesyet.
;;;; youyesyet.canvasser-app.ajax: transciever for ajax packets.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@ -25,13 +27,20 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have
;;; 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 building view.
(defn local-uri? [{:keys [uri]}]
(not (re-find #"^\w+?://" uri)))
(defn default-headers [request]
(if (local-uri? request)
(-> request
(update :uri #(str js/context %))
(update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
request))
(defn load-interceptors! []
(swap! ajax/default-interceptors
conj
(ajax/to-interceptor {:name "default headers"
:request default-headers})))
(defn panel
"Generate the building panel."
[]
[])

View file

@ -1,4 +1,6 @@
(ns youyesyet.core
(ns ^{:doc "Canvasser app navigation and routing."
:author "Simon Brooke"}
youyesyet.canvasser-app.core
(:require cljsjs.react-leaflet
[ajax.core :refer [GET POST]]
[goog.events :as events]
@ -7,21 +9,22 @@
[reagent.core :as r]
[re-frame.core :as rf]
[secretary.core :as secretary]
[youyesyet.ajax :refer [load-interceptors!]]
[youyesyet.handlers]
[youyesyet.subscriptions]
[youyesyet.ui-utils :as ui]
[youyesyet.views.about :as about]
[youyesyet.views.electors :as electors]
[youyesyet.views.followup :as followup]
[youyesyet.views.issue :as issue]
[youyesyet.views.issues :as issues]
[youyesyet.views.map :as maps])
[youyesyet.canvasser-app.ajax :refer [load-interceptors!]]
[youyesyet.canvasser-app.handlers]
[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.followup :as followup]
[youyesyet.canvasser-app.views.issue :as issue]
[youyesyet.canvasser-app.views.issues :as issues]
[youyesyet.canvasser-app.views.map :as maps])
(:import goog.History))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.core: core of the app.
;;;; youyesyet.canvasser-app.core: core of the app.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@ -48,6 +51,9 @@
(defn about-page []
(about/panel))
(defn building-page []
(building/panel))
(defn electors-page []
(electors/panel))
@ -65,6 +71,7 @@
(def pages
{:about #'about-page
:building #'building-page
:electors #'electors-page
:followup #'followup-page
:issues #'issues-page
@ -103,10 +110,10 @@
(secretary/defroute "/about" []
(rf/dispatch [:set-active-page :about]))
(secretary/defroute "/electors" []
(rf/dispatch [:set-active-page :electors]))
(secretary/defroute "/electors/:dwelling" {dwelling-id :dwelling}
(rf/dispatch [:set-dwelling dwelling-id]))
(secretary/defroute "/electors/:address" {address-id :address}
(secretary/defroute "/building/:address" {address-id :address}
(rf/dispatch [:set-address address-id]))
(secretary/defroute "/followup" []

View file

@ -1,7 +1,9 @@
(ns youyesyet.handlers
(ns ^{:doc "Canvasser app event handlers."
:author "Simon Brooke"}
youyesyet.canvasser-app.handlers
(:require [cljs.reader :refer [read-string]]
[re-frame.core :refer [dispatch reg-event-db]]
[youyesyet.db :as db]
[youyesyet.canvasser-app.state :as db]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -55,32 +57,56 @@
(reg-event-db
:send-intention
(fn [db [_ args]]
(let [intention (:intention args)
elector-id (:elector-id args)
elector
(first
(remove nil?
(map
#(if (= elector-id (:id %)) %)
(:electors (:address db)))))
old-address (:address db)
new-address (assoc old-address :electors (cons (assoc elector :intention intention) (remove #(= % elector) (:electors old-address))))]
(cond
(nil? elector)
(assoc db :error "No elector found; not setting intention")
(= intention (:intention elector)) (do (js/console.log "Elector's intention hasn't changed; not setting intention") db)
true
(do
(js/console.log (str "Setting intention of elector " elector " to " intention))
(merge
(clear-messages db)
{:addresses
(cons new-address (remove old-address (:addresses db)))
:address new-address
:elector elector
:outqueue (cons (assoc args :action :set-intention) (:outqueue db))}))))))
:send-intention
(fn [db [_ args]]
(let [intention (:intention args)
elector-id (:elector-id args)
old-elector (first
(remove nil?
(map
#(if (= elector-id (:id %)) %)
(:electors (:dwelling db)))))
new-elector (assoc old-elector :intention intention)
old-dwelling (:dwelling db)
new-dwelling (assoc
old-dwelling
:electors
(cons
new-elector
(remove
#(= % old-elector)
(:electors old-dwelling))))
old-address (:address db)
new-address (assoc
old-address
:dwellings
(cons
new-dwelling
(remove
#(= % old-dwelling)
(:dwellings old-address))))]
(cond
(nil? old-elector)
(assoc db :error "No elector found; not setting intention")
(= intention (:intention old-elector))
(do
(js/console.log "Elector's intention hasn't changed; not setting intention")
db)
true
(do
(js/console.log (str "Setting intention of elector " old-elector " to " intention))
(merge
(clear-messages db)
{:addresses
(cons
new-address
(remove #(= % old-address) (:addresses db)))
:address new-address
:dwelling new-dwelling
:elector new-elector
:outqueue (cons
(assoc args :action :set-intention)
(:outqueue db))}))))))
(reg-event-db
@ -110,7 +136,29 @@
(fn [db [_ address-id]]
(let [id (read-string address-id)
address (first (remove nil? (map #(if (= id (:id %)) %) (:addresses db))))]
(assoc (clear-messages db) :address address :page :electors))))
(if
(= (count (:dwellings address)) 1)
(assoc (clear-messages db)
:address address
:dwelling (first (:dwellings address))
:page :electors)
(assoc (clear-messages db)
:address address
:dwelling nil
:page :building)))))
(reg-event-db
:set-dwelling
(fn [db [_ dwelling-id]]
(let [id (read-string dwelling-id)
dwelling (first
(remove
nil?
(map
#(if (= id (:id %)) %)
(mapcat :dwellings (:addresses db)))))]
(assoc (clear-messages db) :dwelling dwelling :page :electors))))
(reg-event-db
@ -145,8 +193,34 @@
(assoc (clear-messages db) :issue issue)))
(reg-event-db
:set-latitude
(fn [db [_ issue]]
(assoc db :latitude issue)))
(reg-event-db
:set-longitude
(fn [db [_ issue]]
(assoc db :longitude issue)))
(reg-event-db
:set-telephone
(fn [db [_ telephone]]
(js/console.log (str "Setting telephone to " telephone))
(assoc (clear-messages db) :telephone telephone)))
(reg-event-db
:set-view
(fn [db [_ view]]
(assoc db :view view)))
(reg-event-db
:set-zoom
(fn [db [_ zoom]]
(if (integer? zoom)
(assoc db :zoom zoom)
db)))

View file

@ -0,0 +1,89 @@
(ns ^{:doc "Canvasser app client state."
:author "Simon Brooke"}
youyesyet.canvasser-app.state)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.canvasser-app.state: the state of the 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) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; This is the constructor for the atom in which the state of the user interface is held.
;;; The atom gets updated by 'events' registered in handler.cljs, q.v.
;;;
;;; not wonderfully happy with 'db' as a name for this namespace; will probably change to
;;; 'client-state'.
(def default-db
{;;; the currently selected address, if any.
:address {:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2569057
:dwellings [{:id 1
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]}]}
;;; a list of the addresses in the current location at which there
;;; are electors registered.
:addresses [{:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2570944
:dwellings [{:id 1
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]}]}
{:id 2 :address "15 Imaginary Terrace, IM1 3TE" :latitude 55.8252354 :longitude -4.2572778
:dwellings [{:id 2
:electors [{:id 1 :name "Beryl Brown" :gender :female}
{:id 2 :name "Betty Black" :gender :female}]}]}
{:id 3 :address "17 Imaginary Terrace, IM1 3TE" :latitude 55.825166 :longitude -4.257026
:dwellings [{:id 3 :sub-address "Flat 1"
:electors [{:id 1 :name "Catriona Crathie" :gender :female :intention :yes}
{:id 2 :name "Colin Caruthers" :gender :male :intention :yes}
{:id 3 :name "Calum Crathie" :intention :yes}]}
{:id 4 :sub-address "Flat 2"
:electors [{:id 1 :name "David Dewar" :gender :male :intention :no}]}]}]
;;; electors at the currently selected dwelling
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]
;;; any error to display
:error nil
;;; the issue from among the issues which is currently selected.
;;; any confirmation message to display
:feedback nil
;;; the currently selected issue
:issue "Currency"
;;; the issues selected for the issues page on this day.
:issues {"Currency" "Scotland could keep the Pound, or use the Euro. But we could also set up a new currency of our own. Yada yada yada"
"Monarchy" "Scotland could keep the Queen. This is an issue to be decided after independence. Yada yada yada"
"Defence" "Scotland will not have nuclear weapons, and will probably not choose to engage in far-off wars. But we could remain members of NATO"}
;;; message of the day
:motd "This is a test version only. There is no real data."
;;; the options from among which electors can select.
:options [{:id :yes :description "Yes"} {:id :no :description "No"}]
;;; the queue of items waiting to be transmitted.
:outqueue ()
;;; the currently displayed page within the app.
:page :home
:view nil
:latitude 55.82
:longitude -4.25
:zoom 12})

View file

@ -1,4 +1,6 @@
(ns youyesyet.subscriptions
(ns ^{:doc "Canvasser app event subscriptions."
:author "Simon Brooke"}
youyesyet.canvasser-app.subscriptions
(:require [re-frame.core :refer [reg-sub]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -44,6 +46,11 @@
(fn [db _]
(:changes db)))
(reg-sub
:dwelling
(fn [db _]
(:dwelling db)))
(reg-sub
:elector
(fn [db _]
@ -69,6 +76,16 @@
(fn [db _]
(:issues db)))
(reg-sub
:latitude
(fn [db _]
(:latitude db)))
(reg-sub
:longitude
(fn [db _]
(:longitude db)))
(reg-sub
:page
(fn [db _]
@ -84,3 +101,12 @@
(fn [db _]
(:outqueue db)))
(reg-sub
:view
(fn [db _]
(:view db)))
(reg-sub
:zoom
(fn [db _]
(:zoom db)))

View file

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

View file

@ -1,11 +1,13 @@
(ns youyesyet.views.about
(ns ^{:doc "Canvasser app about panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.about
(:require [re-frame.core :refer [reg-sub subscribe]]
[markdown.core :refer [md->html]]
[youyesyet.ui-utils :as ui]))
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.about: about/credits view for youyesyet.
;;;; youyesyet.canvasser-app.views.about: about/credits view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License

View file

@ -0,0 +1,54 @@
(ns ^{:doc "Canvasser app households in building panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.building
(:require [re-frame.core :refer [reg-sub subscribe]]
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.canvasser-app.views.building: building view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The pattern from the re-com demo (https://github.com/Day8/re-com) is to have
;;; one source file/namespace per view. Each namespace contains a function 'panel'
;;; whose output is an enlive-style specification of the view to be redered.
;;; I propose to follow this pattern. This file will provide the building view.
(defn panel
"Generate the building panel."
[]
(let [address @(subscribe [:address])
dwellings (:dwellings address)]
[:div
[:h1 (str "Flats at " (:address address))]
[:div.container {:id "main-container"}
(ui/back-link)
[:div {:id "dwelling-list"}
(map
(fn
[dwelling]
(ui/big-link
(:sub-address dwelling)
(str "#/electors/" (:id dwelling))) )
(sort
#(< (:sub-address %1) (:sub-address %2))
(:dwellings address)))]]]))

View file

@ -1,11 +1,13 @@
(ns youyesyet.views.electors
(ns ^{:doc "Canvasser app electors in household panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.electors
(:require [reagent.core :refer [atom]]
[re-frame.core :refer [reg-sub subscribe dispatch]]
[youyesyet.ui-utils :as ui]))
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.electors: electors view for youyesyet.
;;;; youyesyet.canvasser-app.views.electors: electors view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@ -30,7 +32,7 @@
;;; 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 electors view.
;;; I propose to follow this pattern. This file will provide the electors view.
;;; See https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md#electors-view
@ -105,14 +107,16 @@
(defn panel
"Generate the electors panel."
[]
(let [address @(subscribe [:address])
addresses @(subscribe [:addresses])
electors (sort-by :id (:electors address))
options @(subscribe [:options])
changes @(subscribe [:changes])]
(let [dwelling @(subscribe [:dwelling])
address @(subscribe [:address])
sub-address (:sub-address dwelling)
electors (sort-by :id (:electors dwelling))
options @(subscribe [:options])]
(if address
[:div
[:h1 (:address address)]
[:h1 (if sub-address
(str sub-address ", " (:address address))
(:address address))]
[:div.container {:id "main-container"}
[:table
[:tbody

View file

@ -1,10 +1,12 @@
(ns youyesyet.views.followup
(ns ^{:doc "Canvasser followup request form panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.followup
(:require [re-frame.core :refer [reg-sub subscribe dispatch]]
[youyesyet.ui-utils :as ui]))
[youyesyet.canvasser-app.ui-utils :as ui]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.followup-request: followup-request view for youyesyet.
;;;; youyesyet.canvasser-app.views.followup-request: followup-request view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License

View file

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

View file

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

View file

@ -1,10 +1,12 @@
(ns youyesyet.views.map
(ns ^{:doc "Canvasser app map view panel."
:author "Simon Brooke"}
youyesyet.canvasser-app.views.map
(:require [re-frame.core :refer [reg-sub subscribe dispatch]]
[reagent.core :as reagent]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.views.map: map view for youyesyet.
;;;; youyesyet.canvasser-app.views.map: map view for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
@ -55,7 +57,14 @@
(defn pin-image
"select the name of a suitable pin image for this address"
[address]
(let [intentions (set (remove nil? (map #(:intention %) (:electors address))))]
(let [intentions
(set
(remove
nil?
(map
:intention
(mapcat :electors
(:dwellings address)))))]
(case (count intentions)
0 "unknown-pin"
1 (str (name (first intentions)) "-pin")
@ -68,7 +77,12 @@
so back links work."
[id]
(js/console.log (str "Click handler for address #" id))
(set! window.location.href (str "#electors/" id)))
(let [view @(subscribe [:view])
centre (.getCenter view)]
(dispatch [:set-zoom (.getZoom view)])
(dispatch [:set-latitude (.-lat centre)])
(dispatch [:set-longitude (.-lng centre)]))
(set! window.location.href (str "#building/" id)))
;; This way is probably more idiomatic React, but history doesn't work:
;; (defn map-pin-click-handler
;; [id]
@ -112,7 +126,10 @@
(defn map-did-mount-osm
"Did-mount function loading map tile data from Open Street Map."
[]
(let [view (.setView (.map js/L "map" (clj->js {:zoomControl false})) #js [55.82 -4.25] 13)
(let [view (.setView
(.map js/L "map" (clj->js {:zoomControl false}))
#js [@(subscribe [:latitude]) @(subscribe [:longitude])]
@(subscribe [:zoom]))
addresses @(subscribe [:addresses])]
(js/console.log (str "Adding " (count addresses) " pins"))
(doall (map #(add-map-pin % view) addresses))
@ -120,7 +137,8 @@
(clj->js {:attribution osm-attrib
:maxZoom 18}))
view)
))
(dispatch [:set-view view])
view))
(defn map-did-mount

View file

@ -1,79 +0,0 @@
(ns youyesyet.db)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.db: the state of the 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) 2016 Simon Brooke for Radical Independence Campaign
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; This is the constructor for the atom in which the state of the user interface is held.
;;; The atom gets updated by 'events' registered in handler.cljs, q.v.
;;;
;;; not wonderfully happy with 'db' as a name for this namespace; will probably change to
;;; 'client-state'.
(def default-db
{;;; the currently selected address, if any.
:address {:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2569057
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]}
;;; a list of the addresses in the current location at which there
;;; are electors registered.
:addresses [{:id 1 :address "13 Imaginary Terrace, IM1 3TE" :latitude 55.8253043 :longitude -4.2570944
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]}
{:id 2 :address "15 Imaginary Terrace, IM1 3TE" :latitude 55.8252354 :longitude -4.2572778
:electors [{:id 1 :name "Beryl Brown" :gender :female}
{:id 2 :name "Betty Black" :gender :female}]}
{:id 3 :address "17 Imaginary Terrace, IM1 3TE" :latitude 55.825166 :longitude -4.257026
:electors [{:id 1 :name "Catriona Crathie" :gender :female :intention :yes}
{:id 2 :name "Colin Caruthers" :gender :male :intention :yes}
{:id 3 :name "Calum Crathie" :intention :yes}]}
{:id 4 :address "19 Imaginary Terrace, IM1 3TE" :latitude 55.82506950000001 :longitude -4.2570239
:electors [{:id 1 :name "David Dewar" :gender :male :intention :no}]}]
;;; electors at the currently selected address
:electors [{:id 1 :name "Alan Anderson" :gender :male :intention :no}
{:id 2 :name "Ann Anderson" :gender :female}
{:id 3 :name "Alex Anderson" :gender :fluid :intention :yes}
{:id 4 :name "Andy Anderson" :intention :yes}]
;;; any error to display
:error nil
;;; the issue from among the issues which is currently selected.
;;; any confirmation message to display
:feedback nil
;;; the currently selected issue
:issue "Currency"
;;; the issues selected for the issues page on this day.
:issues {"Currency" "Scotland could keep the Pound, or use the Euro. But we could also set up a new currency of our own. Yada yada yada"
"Monarchy" "Scotland could keep the Queen. This is an issue to be decided after independence. Yada yada yada"
"Defence" "Scotland will not have nuclear weapons, and will probably not choose to engage in far-off wars. But we could remain members of NATO"}
;;; message of the day
:motd "This is a test version only. There is no real data."
;;; the options from among which electors can select.
:options [{:id :yes :description "Yes"} {:id :no :description "No"}]
;;; the queue of items waiting to be transmitted.
:outqueue ()
;;; the currently displayed page within the app.
:page :home
})

View file

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

View file

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

View file

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

392
youyesyet.adl.xml Normal file
View file

@ -0,0 +1,392 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- DOCTYPE application PUBLIC "-//JOURNEYMAN//DTD ADL 1.4//EN"
"http://www.journeyman.cc/adl/stable/adl/schemas/adl-1.4.dtd" -->
<application name="youyesyet" version="0.1.1"
xmlns="http://bowyer.journeyman.cc/adl/1.4/"
xmlns:adl="http://bowyer.journeyman.cc/adl/1.4/">
<documentation>
A web-app intended to be used by canvassers campaigning for a 'Yes' vote in the second independence referendum.
The web-app will be delivered to canvassers out knocking doors primarily through an HTML5/React single-page app designed to work on a mobile phone; it's possible that someone else may do an Android of iPhone native app to address the same back end but at present I have no plans for this.
There must also be an administrative interface through which privileged users can set the system up and authorise canvassers, and a 'followup' interface through which issue-expert specialist canvassers can address particular electors' queries.
</documentation>
<content>
<head> </head>
<top> </top>
<foot> </foot>
</content>
<typedef name="postcode" type="string"
pattern="^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$" size="16">
<documentation>See
https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/488478/Bulk_Data_Transfer_-_additional_validation_valid_from_12_November_2015.pdf,
section 3</documentation>
<help locale="en-GB">A valid postcode.</help>
</typedef>
<group name="public">
<documentation>All users</documentation>
</group>
<group name="canvassers" parent="public">
<documentation>All users of the canvasser app Able to read and add canvassing data in a limited
radius around their current position. </documentation>
</group>
<group name="teamorganiser" parent="canvassers">
<documentation>Organisers of canvassing teams Able to see and modify data on the canvassers in
the team(s) they organise; able to add canvassers to their team; able to update canvassers in
their team, including resetting passwords and locking accounts; able to see canvass data over
the whole area in which their team operates. </documentation>
</group>
<group name="issueexperts" parent="public">
<documentation>People expert on particular issues. Able to read followup requests, and the electors to which they
relate; able to access (read/write) the issues wiki; able to write followuop action records.
</documentation>
</group>
<group name="analysts" parent="public">
<documentation>Users entitled to see an overview of the canvassing data collected. Able to read canvassing data over the whole map, including historical
data.</documentation>
</group>
<group name="issueeditors" parent="analysts">
<documentation>Users responsible for determining what issues should be current at any time.
Able to set current issues; able to add issues.</documentation>
</group>
<group name="admin" parent="public">
<documentation>Able to read and update canvasser records, team membership records, team
organisership records, issue expertise records; able to add and update reference data
generally.</documentation>
</group>
<entity name="electors">
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property size="64" required="true" type="string" name="name" column="name" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<property required="true" type="entity" name="address_id" column="address_id" entity="addresses"
farkey="id">
<prompt prompt="address_id" locale="en-GB"/>
</property>
<property size="16" type="string" name="phone" column="phone" distinct="user">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property size="128" type="string" name="email" column="email" distinct="user">
<prompt prompt="email" locale="en-GB"/>
</property>
<property name="gender" type="entity" column="gender" entity="genders" farkey="id"
default="Unknown">
<prompt prompt="gender" locale="en-GB"/>
</property>
<list name="Electors" properties="all"/>
<form name="Elector" properties="all"/>
</entity>
<entity name="genders" natural-key="id">
<key>
<property name="id" type="string" size="32" distinct="all">
<prompt prompt="gender" locale="en-GB"/>
</property>
</key>
<list name="Genders" properties="all"/>
<form name="Gender" properties="all"/>
</entity>
<entity name="addresses">
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="address" column="address" size="256" distinct="user">
<prompt prompt="address" locale="en-GB"/>
</property>
<property type="defined" typedef="postcode" name="postcode" column="postcode" size="16" distinct="user">
<prompt prompt="postcode" locale="en-GB"/>
</property>
<property type="string" name="phone" column="phone" size="16">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property type="entity" name="district_id" column="district_id" entity="districts" farkey="id">
<prompt prompt="district_id" locale="en-GB"/>
</property>
<property type="real" name="latitude" column="latitude">
<prompt prompt="latitude" locale="en-GB"/>
</property>
<property type="real" name="longitude" column="longitude">
<prompt prompt="longitude" locale="en-GB"/>
</property>
<permission group="admin" permission="all"/>
<permission group="canvassers" permission="read"/>
<permission group="analysts" permission="read"/>
<list name="Addresses" properties="all"/>
<form name="Address" properties="all"/>
</entity>
<entity name="visits">
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="integer" name="address_id" column="address_id"
entity="addresses" farkey="id">
<prompt prompt="address_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="canvasser_id" column="canvasser_id"
entity="canvassers" farkey="id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<property required="true" default="" type="timestamp" name="date" column="date">
<prompt prompt="date" locale="en-GB"/>
</property>
<permission group="public" permission="read"/>
<permission group="canvassers" permission="insert"/>
</entity>
<entity name="authorities">
<key>
<property required="true" type="string" name="id" column="id" size="32" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
<entity name="issues">
<documentation>Issues believed to be of interest to electors, about which they may have questions.</documentation>
<key>
<property required="true" type="string" name="id" column="id" size="32" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property type="string" name="url" column="url" size="256">
<prompt prompt="url" locale="en-GB"/>
</property>
<property name="current" type="boolean" default="true">
<prompt prompt="Is current?" locale="en-GB"/>
</property>
<list name="Issues" properties="all"/>
<form name="Issue" properties="all"/>
</entity>
<entity name="intentions">
<documentation>Link table.</documentation>
<property required="true" type="entity" entity="visits" farkey="id" name="visit_id"
column="visit_id">
<prompt prompt="visit_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="electors" farkey="id" name="elector_id"
column="elector_id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="options" farkey="id" name="option_id"
column="option_id">
<prompt prompt="option_id" locale="en-GB"/>
</property>
</entity>
<entity name="canvassers">
<documentation>Primary users of the system: those actually interviewing electors.</documentation>
<key>
<property type="integer" name="id" column="id" distinct="system" immutable="true">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="username" column="username" size="32" distinct="all">
<prompt prompt="username" locale="en-GB"/>
</property>
<property required="true" type="string" name="fullname" column="fullname" size="64" distinct="user">
<prompt prompt="fullname" locale="en-GB"/>
</property>
<property type="entity" name="elector_id" column="elector_id" entity="electors" farkey="id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="integer" name="address_id" column="address_id"
entity="addresses" farkey="id">
<prompt prompt="address_id" locale="en-GB"/>
</property>
<property type="string" name="phone" column="phone" size="16">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property type="string" name="email" column="email" size="128" distinct="user">
<prompt prompt="email" locale="en-GB"/>
</property>
<property required="true" type="entity" name="authority_id"
entity="authorities" farkey="id">
<prompt prompt="authority_id" locale="en-GB"/>
</property>
<property type="boolean" name="authorised" column="authorised">
<prompt prompt="authorised" locale="en-GB"/>
</property>
<permission group="canvassers" permission="edit">
<documentation>But only their own record</documentation>
</permission>
<permission group="teamleaders" permission="edit">
<documentation>But only canvassers in their own team.</documentation>
</permission>
<permission group="admin" permission="all">
<documentation>All canvassers</documentation>
</permission>
</entity>
<entity name="followuprequests">
<documentation>Requests for a followup with an issue expert</documentation>
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="entity" name="elector_id" column="elector_id" entity="electors"
farkey="id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="visit_id" column="visit_id" entity="visits"
farkey="id">
<prompt prompt="visit_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="issue_id" column="issue_id" entity="issues"
farkey="id">
<prompt prompt="issue_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="method_id" column="method_id"
entity="followupmethods" farkey="id">
<prompt prompt="method_id" locale="en-GB"/>
</property>
</entity>
<entity name="rolememberships">
<documentation>Link table</documentation>
<property required="true" type="entity" entity="roles" farkey="id" name="role_id"
column="role_id">
<prompt prompt="role_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id"
column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
</entity>
<entity name="roles">
<documentation>A role (essentially, the same as a group, but application layer rather than database layer) of which a user may be a member.</documentation>
<key>
<property type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user" immutable="true">
<prompt prompt="name" locale="en-GB"/>
</property>
</entity>
<entity name="teams">
<key>
<property type="integer" name="id" column="id" distinct="system" immutable="true">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="districts" farkey="id" name="district_id"
column="district_id">
<prompt prompt="district_id" locale="en-GB"/>
</property>
<property type="real" name="latitude" column="latitude">
<prompt prompt="latitude" locale="en-GB"/>
</property>
<property type="real" name="longitude" column="longitude">
<prompt prompt="longitude" locale="en-GB"/>
</property>
<permission group="canvassers" permission="read"/>
<permission group="groupleaders" permission="edit">
<documentation>But only their own group(s)</documentation>
</permission>
<permission group="admin" permission="all">
<documentation>All groups</documentation>
</permission>
</entity>
<entity name="districts">
<documentation>Electoral districts</documentation>
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<permission group="public" permission="read"></permission>
<permission group="admin" permission="all"></permission>
</entity>
<entity name="teamorganiserships">
<documentation>Link table</documentation>
<property required="true" type="entity" entity="teams" farkey="id" name="team_id"
column="team_id">
<prompt prompt="team_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id"
column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
</entity>
<entity name="followupactions">
<documentation>Actions taken on followup requests.</documentation>
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="entity" name="request_id" column="request_id"
entity="followuprequests" farkey="id">
<prompt prompt="request_id" locale="en-GB"/>
</property>
<property required="true" type="integer" name="actor" column="actor" entity="canvassers"
farkey="id">
<prompt prompt="actor" locale="en-GB"/>
</property>
<property required="true" default="" type="timestamp" name="date" column="date">
<prompt prompt="date" locale="en-GB"/>
</property>
<property type="text" name="notes" column="notes">
<prompt prompt="notes" locale="en-GB"/>
</property>
<property type="boolean" name="closed" column="closed" default="false">
<prompt prompt="closed" locale="en-GB"/>
</property>
<permission group="issueexperts" permission="insert"/>
<permission group="canvassers" permission="read">
<documentation>But only for electors in their immediate vicinity</documentation>
</permission>
<permission group="analysts" permission="read"/>
</entity>
<entity name="issueexpertise">
<documentation>Link table</documentation>
<property required="true" type="integer" name="canvasser_id" column="canvasser_id"
entity="canvassers" farkey="id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="issue_id" column="issue_id" entity="issues"
farkey="id">
<prompt prompt="issue_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="method_id" column="method_id"
entity="followupmethods" farkey="id">
<prompt prompt="method_id" locale="en-GB"/>
</property>
</entity>
<entity name="options">
<documentation>Options in the election or referendum being canvassed on</documentation>
<key>
<property required="true" type="string" name="id" column="id" size="32" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
<entity name="teammemberships">
<documentation>Link table</documentation>
<property required="true" type="entity" entity="teams" farkey="id" name="team_id"
column="team_id">
<prompt prompt="team_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id"
column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
</entity>
<entity name="followupmethods">
<key>
<property required="true" type="string" size="32" name="id" column="id" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
</application>

528
youyesyet.canonical.adl.xml Normal file
View file

@ -0,0 +1,528 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- DOCTYPE application PUBLIC "-//JOURNEYMAN//DTD ADL 1.4//EN"
"http://www.journeyman.cc/adl/stable/adl/schemas/adl-1.4.dtd" -->
<application xmlns="http://bowyer.journeyman.cc/adl/1.4/" xmlns:adl="http://bowyer.journeyman.cc/adl/1.4/" name="youyesyet" version="0.1.1">
<!--
***************************************************************************
*
* Application Description Language Framework
* ©2007 Cygnet Solutions Ltd
*
* THIS FILE IS AUTOMATICALLY GENERATED AND SHOULD NOT
* BE MANUALLY EDITED.
*
* Generated using adl2canonical.xslt 1.10 $
*
***************************************************************************
-->
<documentation>
A web-app intended to be used by canvassers campaigning for a 'Yes' vote in the second independence referendum.
The web-app will be delivered to canvassers out knocking doors primarily through an HTML5/React single-page app designed to work on a mobile phone; it's possible that someone else may do an Android of iPhone native app to address the same back end but at present I have no plans for this.
There must also be an administrative interface through which privileged users can set the system up and authorise canvassers, and a 'followup' interface through which issue-expert specialist canvassers can address particular electors' queries.
</documentation>
<content>
<head> </head>
<top> </top>
<foot> </foot>
</content>
<typedef name="postcode" type="string" pattern="^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$" size="16">
<documentation>See
https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/488478/Bulk_Data_Transfer_-_additional_validation_valid_from_12_November_2015.pdf,
section 3</documentation>
<help locale="en-GB">A valid postcode.</help>
</typedef>
<group name="public">
<documentation>All users</documentation>
</group>
<group name="canvassers" parent="public">
<documentation>All users of the canvasser app Able to read and add canvassing data in a limited
radius around their current position. </documentation>
</group>
<group name="teamorganiser" parent="canvassers">
<documentation>Organisers of canvassing teams Able to see and modify data on the canvassers in
the team(s) they organise; able to add canvassers to their team; able to update canvassers in
their team, including resetting passwords and locking accounts; able to see canvass data over
the whole area in which their team operates. </documentation>
</group>
<group name="issueexperts" parent="public">
<documentation>People expert on particular issues. Able to read followup requests, and the electors to which they
relate; able to access (read/write) the issues wiki; able to write followuop action records.
</documentation>
</group>
<group name="analysts" parent="public">
<documentation>Users entitled to see an overview of the canvassing data collected. Able to read canvassing data over the whole map, including historical
data.</documentation>
</group>
<group name="issueeditors" parent="analysts">
<documentation>Users responsible for determining what issues should be current at any time.
Able to set current issues; able to add issues.</documentation>
</group>
<group name="admin" parent="public">
<documentation>Able to read and update canvasser records, team membership records, team
organisership records, issue expertise records; able to add and update reference data
generally.</documentation>
</group>
<!--
entity electors already has a key - not generating one
-->
<entity name="electors">
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property size="64" required="true" type="string" name="name" column="name" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<property required="true" type="entity" name="dwelling_id" column="dwelling_id" entity="dwelling" farkey="id">
<prompt prompt="Flat" locale="en-GB"/>
</property>
<property size="16" type="string" name="phone" column="phone" distinct="user">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property size="128" type="string" name="email" column="email" distinct="user">
<prompt prompt="email" locale="en-GB"/>
</property>
<property name="gender" type="entity" column="gender" entity="genders" farkey="id" default="Unknown">
<prompt prompt="gender" locale="en-GB"/>
</property>
<list properties="listed" name="Electors"><field property="id"/><field property="name"/><field property="address_id"/><field property="phone"/><field property="email"/><field property="gender"/></list>
<form properties="listed" name="Elector"><field property="id"/><field property="name"/><field property="address_id"/><field property="phone"/><field property="email"/><field property="gender"/></form>
</entity>
<entity name="genders" natural-key="id">
<key>
<property name="id" type="string" size="32" distinct="all">
<prompt prompt="gender" locale="en-GB"/>
</property>
</key>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
<list properties="listed" name="Genders">
<field property="id"/>
</list>
<form properties="listed" name="Gender">
<field property="id"/>
</form>
</entity>
<entity name="dwellings">
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="entity" name="address_id" column="address_id" entity="address" farkey="id">
<prompt prompt="Building Address" locale="en-GB"/>
</property>
<property required="false" type="string" size="32" name="sub-address"/>
</entity>
<!--
entity addresses already has a key - not generating one
-->
<entity name="addresses">
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="address" column="address" size="256" distinct="user">
<prompt prompt="address" locale="en-GB"/>
</property>
<property type="defined" typedef="postcode" name="postcode" column="postcode" size="16" distinct="user">
<prompt prompt="postcode" locale="en-GB"/>
</property>
<property type="string" name="phone" column="phone" size="16">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property type="entity" name="district_id" column="district_id" entity="districts" farkey="id">
<prompt prompt="district_id" locale="en-GB"/>
</property>
<property type="real" name="latitude" column="latitude">
<prompt prompt="latitude" locale="en-GB"/>
</property>dwe
<property type="real" name="longitude" column="longitude">
<prompt prompt="longitude" locale="en-GB"/>
</property>
<permission group="admin" permission="all"/>
<permission group="canvassers" permission="read"/>
<permission group="analysts" permission="read"/>
<list properties="listed" name="Addresses"><field property="id"/><field property="address"/><field property="postcode"/><field property="phone"/><field property="district_id"/><field property="latitude"/><field property="longitude"/></list>
<form properties="listed" name="Address"><field property="id"/><field property="address"/><field property="postcode"/><field property="phone"/><field property="district_id"/><field property="latitude"/><field property="longitude"/></form>
</entity>
<!--
entity visits already has a key - not generating one
-->
<entity name="visits">
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="integer" name="address_id" column="address_id" entity="addresses" farkey="id">
<prompt prompt="address_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="canvasser_id" column="canvasser_id" entity="canvassers" farkey="id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<property required="true" default="" type="timestamp" name="date" column="date">
<prompt prompt="date" locale="en-GB"/>
</property>
<permission group="public" permission="read"/>
<permission group="canvassers" permission="insert"/>
</entity>
<!--
entity authorities already has a key - not generating one
-->
<entity name="authorities">
<key>
<property required="true" type="string" name="id" column="id" size="32" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
<!--
entity issues already has a key - not generating one
-->
<entity name="issues">
<documentation>Issues believed to be of interest to electors, about which they may have questions.</documentation>
<key>
<property required="true" type="string" name="id" column="id" size="32" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property type="string" name="url" column="url" size="256">
<prompt prompt="url" locale="en-GB"/>
</property>
<property name="current" type="boolean" default="true">
<prompt prompt="Is current?" locale="en-GB"/>
</property>
<list properties="listed" name="Issues"><field property="id"/><field property="url"/><field property="current"/></list>
<form properties="listed" name="Issue"><field property="id"/><field property="url"/><field property="current"/></form>
</entity>
<!--
entity intentions has no key - generating one
-->
<entity name="intentions" table="intentions">
<documentation>Link table.</documentation>
<key>
<property type="integer" distinct="system" required="true" name="Id">
<generator action="native"/>
<documentation>
Auto-generated abstract primary key
</documentation>
</property>
</key>
<property required="true" type="entity" entity="visits" farkey="id" name="visit_id" column="visit_id">
<prompt prompt="visit_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="electors" farkey="id" name="elector_id" column="elector_id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="options" farkey="id" name="option_id" column="option_id">
<prompt prompt="option_id" locale="en-GB"/>
</property>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
</entity>
<!--
entity canvassers already has a key - not generating one
-->
<entity name="canvassers">
<documentation>Primary users of the system: those actually interviewing electors.</documentation>
<key>
<property type="integer" name="id" column="id" distinct="system" immutable="true">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="username" column="username" size="32" distinct="all">
<prompt prompt="username" locale="en-GB"/>
</property>
<property required="true" type="string" name="fullname" column="fullname" size="64" distinct="user">
<prompt prompt="fullname" locale="en-GB"/>
</property>
<property type="entity" name="elector_id" column="elector_id" entity="electors" farkey="id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="integer" name="address_id" column="address_id" entity="addresses" farkey="id">
<prompt prompt="address_id" locale="en-GB"/>
</property>
<property type="string" name="phone" column="phone" size="16">
<prompt prompt="phone" locale="en-GB"/>
</property>
<property type="string" name="email" column="email" size="128" distinct="user">
<prompt prompt="email" locale="en-GB"/>
</property>
<property required="true" type="entity" name="authority_id" entity="authorities" farkey="id">
<prompt prompt="authority_id" locale="en-GB"/>
</property>
<property type="boolean" name="authorised" column="authorised">
<prompt prompt="authorised" locale="en-GB"/>
</property>
<permission group="canvassers" permission="edit">
<documentation>But only their own record</documentation>
</permission>
<permission group="teamleaders" permission="edit">
<documentation>But only canvassers in their own team.</documentation>
</permission>
<permission group="admin" permission="all">
<documentation>All canvassers</documentation>
</permission>
</entity>
<!--
entity followuprequests already has a key - not generating one
-->
<entity name="followuprequests">
<documentation>Requests for a followup with an issue expert</documentation>
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="entity" name="elector_id" column="elector_id" entity="electors" farkey="id">
<prompt prompt="elector_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="visit_id" column="visit_id" entity="visits" farkey="id">
<prompt prompt="visit_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="issue_id" column="issue_id" entity="issues" farkey="id">
<prompt prompt="issue_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="method_id" column="method_id" entity="followupmethods" farkey="id">
<prompt prompt="method_id" locale="en-GB"/>
</property>
</entity>
<!--
entity rolememberships has no key - generating one
-->
<entity name="rolememberships" table="rolememberships">
<documentation>Link table</documentation>
<key>
<property type="integer" distinct="system" required="true" name="Id">
<generator action="native"/>
<documentation>
Auto-generated abstract primary key
</documentation>
</property>
</key>
<property required="true" type="entity" entity="roles" farkey="id" name="role_id" column="role_id">
<prompt prompt="role_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id" column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
</entity>
<!--
entity roles already has a key - not generating one
-->
<entity name="roles">
<documentation>A role (essentially, the same as a group, but application layer rather than database layer) of which a user may be a member.</documentation>
<key>
<property type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user" immutable="true">
<prompt prompt="name" locale="en-GB"/>
</property>
</entity>
<!--
entity teams already has a key - not generating one
-->
<entity name="teams">
<key>
<property type="integer" name="id" column="id" distinct="system" immutable="true">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="districts" farkey="id" name="district_id" column="district_id">
<prompt prompt="district_id" locale="en-GB"/>
</property>
<property type="real" name="latitude" column="latitude">
<prompt prompt="latitude" locale="en-GB"/>
</property>
<property type="real" name="longitude" column="longitude">
<prompt prompt="longitude" locale="en-GB"/>
</property>
<permission group="canvassers" permission="read"/>
<permission group="groupleaders" permission="edit">
<documentation>But only their own group(s)</documentation>
</permission>
<permission group="admin" permission="all">
<documentation>All groups</documentation>
</permission>
</entity>
<!--
entity districts already has a key - not generating one
-->
<entity name="districts">
<documentation>Electoral districts</documentation>
<key>
<property required="true" type="integer" name="id" column="id" immutable="true" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="string" name="name" column="name" size="64" distinct="user">
<prompt prompt="name" locale="en-GB"/>
</property>
<permission group="public" permission="read"/>
<permission group="admin" permission="all"/>
</entity>
<!--
entity teamorganiserships has no key - generating one
-->
<entity name="teamorganiserships" table="teamorganiserships">
<documentation>Link table</documentation>
<key>
<property type="integer" distinct="system" required="true" name="Id">
<generator action="native"/>
<documentation>
Auto-generated abstract primary key
</documentation>
</property>
</key>
<property required="true" type="entity" entity="teams" farkey="id" name="team_id" column="team_id">
<prompt prompt="team_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id" column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
</entity>
<!--
entity followupactions already has a key - not generating one
-->
<entity name="followupactions">
<documentation>Actions taken on followup requests.</documentation>
<key>
<property required="true" type="integer" name="id" column="id" distinct="system">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
<property required="true" type="entity" name="request_id" column="request_id" entity="followuprequests" farkey="id">
<prompt prompt="request_id" locale="en-GB"/>
</property>
<property required="true" type="integer" name="actor" column="actor" entity="canvassers" farkey="id">
<prompt prompt="actor" locale="en-GB"/>
</property>
<property required="true" default="" type="timestamp" name="date" column="date">
<prompt prompt="date" locale="en-GB"/>
</property>
<property type="text" name="notes" column="notes">
<prompt prompt="notes" locale="en-GB"/>
</property>
<property type="boolean" name="closed" column="closed" default="false">
<prompt prompt="closed" locale="en-GB"/>
</property>
<permission group="issueexperts" permission="insert"/>
<permission group="canvassers" permission="read">
<documentation>But only for electors in their immediate vicinity</documentation>
</permission>
<permission group="analysts" permission="read"/>
</entity>
<!--
entity issueexpertise has no key - generating one
-->
<entity name="issueexpertise" table="issueexpertise">
<documentation>Link table</documentation>
<key>
<property type="integer" distinct="system" required="true" name="Id">
<generator action="native"/>
<documentation>
Auto-generated abstract primary key
</documentation>
</property>
</key>
<property required="true" type="integer" name="canvasser_id" column="canvasser_id" entity="canvassers" farkey="id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="issue_id" column="issue_id" entity="issues" farkey="id">
<prompt prompt="issue_id" locale="en-GB"/>
</property>
<property required="true" type="entity" name="method_id" column="method_id" entity="followupmethods" farkey="id">
<prompt prompt="method_id" locale="en-GB"/>
</property>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
</entity>
<!--
entity options already has a key - not generating one
-->
<entity name="options">
<documentation>Options in the election or referendum being canvassed on</documentation>
<key>
<property required="true" type="string" name="id" column="id" size="32" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
<!--
entity teammemberships has no key - generating one
-->
<entity name="teammemberships" table="teammemberships">
<documentation>Link table</documentation>
<key>
<property type="integer" distinct="system" required="true" name="Id">
<generator action="native"/>
<documentation>
Auto-generated abstract primary key
</documentation>
</property>
</key>
<property required="true" type="entity" entity="teams" farkey="id" name="team_id" column="team_id">
<prompt prompt="team_id" locale="en-GB"/>
</property>
<property required="true" type="entity" entity="canvassers" farkey="id" name="canvasser_id" column="canvasser_id">
<prompt prompt="canvasser_id" locale="en-GB"/>
</property>
<permission permission="none" group="public"/>
<permission permission="none" group="canvassers"/>
<permission permission="none" group="teamorganiser"/>
<permission permission="none" group="issueexperts"/>
<permission permission="none" group="analysts"/>
<permission permission="none" group="issueeditors"/>
<permission permission="none" group="admin"/>
</entity>
<!--
entity followupmethods already has a key - not generating one
-->
<entity name="followupmethods">
<key>
<property required="true" type="string" size="32" name="id" column="id" immutable="true" distinct="all">
<prompt prompt="id" locale="en-GB"/>
</property>
</key>
</entity>
</application>