Essentially we have six levels of authorisation, at essentially increasing levels of sensitivity.
-
-
-
Canvassers: Any authenticated user essentially has this level of authorisation. Hence users of the app can all share the same database connections without problem. Therefore there will be one first-class database user for all canvassers, and they will not have individual real database logins.
-
-
Issue experts: Issue experts respond to followup requests. Therefore they must be able to see the queue of requests and the details of the elector making the request. They don’t need to see voter intentions and I don’t believe the information they do need to see is particularly sensitive. So they too can share a single database-layer login and connection pool; whether this is the same login as used by the canvassers is an implementation detail but I don’t believe that it’s critical.
-
-
Issue editors: Don’t need to see much sensitive data (although they do need to see, in aggregate, what issues are being raised by electors in the field), but they do have the power to dictate the initial responses canvassers make to issues raised, so the information they can write is pretty sensitive. We need to be very sure that unauthorised users don’t have the power to write this data. So I suggest that issue editors probably should have individual first class database logins.
-
-
Team leaders: Need to be able to monitor the performance of their teams, to invite new users to the system and to block abusive users from the system. Again, these are significant functions which should be well protected from abuse. But we will have at least hundreds, probably thousands of team leaders across Scotland. I would prefer that they each had first class logins, but this may be impractical. But in any case, even if they use a shared login, it should not be the same shared login as used by canvassers.
-
-
Analyists Need broad authorisation to read, but not write or edit, all sensitive data held by the system. They must have individual first class database logins.
-
-
Admins Can necessarily read and write everything. They should definitely each have individual first class database logins.
-
-
This means we have a hybrid authentication scheme; for lower levels, application layer security and shared connection pools are adequate. For higher levels, individual connections and database layer authorisation are required. It implies that the routes at the different layers should be separated into separate namespaces with separate authentication functions.
\ No newline at end of file
diff --git a/docs/competitors.html b/docs/competitors.html
deleted file mode 100644
index 88b37dd..0000000
--- a/docs/competitors.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Competitor Analysis
Obviously You Yes Yet? is my baby; I’ve put a lot of thought into it. At the time I started working on it I wasn’t aware of any open source competitors; I did to a web search, and I emailed the Bernie Sanders campaign to see whether their widely admired tools were open source. I didn’t find anything.
-
However, I’ve just been pointed to Vote Leave’s Vics tool, and there may well be others.
-
There is no room here for ego. What matters is that the Yes campaign gets the best available tool for the job. So it’s important to do competitor analysis, and not to invest too much work into You Yes Yet? unless there’s a realistic possibility of producing a tool which is better than any of the available alternatives. But it’s also the case that by studying competitors we may find ways to improve the design of You Yes Yet?.
-
Vics
-
Vics, the Voter Intention Collection System, is reputed to have been a significant factor in the successful campaign by Vote Leave to take Britain out of the EU. It has been released as open source under MIT licence, so it is unambiguously available for us to use.
-
The architecture comprises a single-page app built using Angular talking to a server built in Java using the Spring framework. The database engine used is Postgres. Jedis, a Java port of Redis, is used as an in-memory data cache, server side.
-
### Download and initial build
-
I checked out the source from the GitHub repository, and following the instructions in the README created the database and ran a maven install process. Unfortunately, run as a normal user, when this process goes into its test sequence many tests fail unable to contact Jedis. I find it slightly worrying to run such a large and complex build as root, but as root it gets substantially further. The build still doesn’t complete but it seems that it is closer to completion.
-
The ironic point is that it fails because it depends on the JavaScript package manager bower, and bower (very sensibly) refuses to run as root. I therefore made a small modification to the build script to allow it to run bower as root, but unfortunately that didn’t solve the build problem; the jedis service was still not found where it was expected.
-
This is difficult to diagnose; the exception is so deeply nested in framework code that no code from the actual Vics application appears on the stack dump, which makes it very hard to know where to start in debugging.
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
-);
-
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
deleted file mode 100644
index 48b5683..0000000
--- a/docs/index.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-Youyesyet 0.2.2-SNAPSHOT
Suppose the YouYesYet project works and we have thousands or tens of thousands of volunteers across Scotland all out chapping doors at the same time: how do we ensure the system stays up under load?
-
Sizing the problem
-
There’s no point in building the app if it will break down under load. We need to be persuaded that it is possible to support the maximum predictable load the system might experience.
-
Database load per volunteer
-
A street canvasser visits on average while working not more than one dwelling every two minutes; the average doorknock-to-doorknock time is probably more like five minutes. Each visit results in
-
-
Zero or one visit record being created;
-
Zero to about five intention records;
-
Zero to about five followup request records.
-
-
So in aggregate minimum zero, maximum about eleven records, typical probably one visit, two intentions = three database inserts per street volunteer per visit. Telephone canvassers probably achieve slightly more because they don’t have to walk from door to door. But over all we’re looking at an average of less than one insert per volunteer per minute.
-
Database reads are probably more infrequent. Each client will obviously need to download the data for each dwelling visited, but it will download these in geograhic blocks of probably around 100 dwellings, and will download a new block only when the user goes outside the area of previously downloaded blocks. However, there ideally should be frequent updates so that the canvasser can see which dwellings other members of the team have already visited, in order that the same dwelling is not visited repeatedly. So there’s probably on average one database read per visit.
-
Reliability of network links
-
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
-
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.
-
Total number of volunteers
-
Personally I’ve never worked in a big canvassing team - maximum about forty people. I believe that there were bigger teams in some parts of urban Scotland. I would guess that the maximum number of volunteers canvassing at any one time - across all groups campaigning for ‘Yes’ in the first independence referendum - never exceeded 35,000 and was probably much lower. I’ve asked whether anyone has better figures but until I have a better estimate I’m going to work on the basis of 35,000 maximum concurrent users.
-
Estimated peak transactions per second
-
This means that the maximum number of transactions per second across Scotland is about
-
35,000 * (1 + 0.2)
------------------- = 700 transactions per second
- 60
-
-
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
-
Caching and memoizing
-
People typically go out canvassing in teams; each member of the team will need the same elector data.
-
Glasgow has a population density of about 3,260 per Km^2; that means each half kilometer square has a maximum population of not much more than 1,000. Downloading 1,000 elector records at startup time is not infeasible. If we normalise data requests to a 100 metre square grid and serve records in 500 metre square chunks, all the members of the same team will request the same chunk of data. Also, elector data is not volatile. Therefore it makes sense to memoize requests for elector data. The app should only request fresh elector data when the device moves within 100 metres of the edge of the current 500 metre cell.
-
Intention data is volatile: we’ll want to update canvassers with fresh intention data frequently, because the other members of their team will be recording intention data as they work, and it’s by seeing that intention data that the canvassers know which doors are still unchapped. So we don’t want to cache intention data for very long. But nevertheless it still makes sense to deliver it in normalised 500 metre square chunks, because that means we can temporarily cache it server side and do not actually have to hit the database with many requests for the same data.
-
Finally, issue data is not volatile over the course of a canvassing session, although it may change over a period of days. So issue data - all the current issues - should be fetched once at app startup time, and not periodically refreshed during a canvassing session. Also, of course, every canvasser will require exactly the same issue data (unless we start thinking of local or regional issues…?), so it absolutely makes sense to memoise requests for issue data.
-
All this normalisation and memoisation reduces the number of read requests on the database.
-
Note that 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.
-
So we could have one database master for each electoral district (or contiguous group of districts) with no real problems except that volunteers working at the very edge of an electoral district would only be supported to canvas on one side of the boundary. I’d rather find an architectural solution which works for the whole of Scotland, but if we cannot do that it isn’t a crisis.
-
It also should not be terribly difficult to organise for a street canvasser user using the Map View to be connected automatically to right geographic shard server, without any action by the user. The issue for telephone canvasser users is a bit different because they will often - perhaps typically - be canvassing voters in a region distant from where they are physically located, so if the geographic sharding model is adopted there would probably have to be an additional electoral district selection screen in the telephone canvasser’s interface.
-
Data from many ‘front-line’ database servers each serving a restricted geographic area can relatively simply be aggregated into a national server by doing the integration work in the wee sma’ oors, when most volunteers (and voters) are asleep.
-
The geographic sharding strategy is scalable. We could start with a single server, split it into a ‘west server’ and an ‘east server’ when that gets overloaded, and further subdivide as needed through the campaign. But we can only do this effectively if we have prepared and tested the strategy in advance.
-
But having considerable numbers of database servers will have cost implications.
-
Geographic sharding by DNS
-
When I first thought of geographic sharding, I intended sharding by electoral district, but actually that makes no sense because electoral districts are complex polygons, which makes point-within-polygon computationally expensive. 4 degrees west falls just west of Stirling, and divides the country in half north-south. 56 degrees north runs north of Edinburgh and Glasgow, but just south of Falkirk. It divides the country in half east to west. Very few towns (and no large towns) straddle either line. Thus we can divide Scotland neatly into four, and it is computationally extremely cheap to compute which shard each data item should be despatched to.
giving us an incredibly simple dispatch table. Furthermore, initially all four addresses can point to the same server. This is an incredibly simple scheme, and I’m confident it’s good enough.
-
Data that’s inserted from the canvassing app - that is to say, voter intention data and followup request data - should have an additional field ‘shard’ (char(2)) which should hold the digraph representing the shard to which it was dispatched, and that field should form part of the primary key, allowing the data from all servers to be integrated. Data that isn’t from the canvassing app should probably be directed to the ‘nw’ shard (which will be lightest loaded), or to a separate master server, and then all servers should be synced overnight.
-
Read servers and write servers
-
It’s a common practice in architecting busy web systems to have one master database server to which all write operations are directed, surrounded by a ring of slave databases which replicate from the master and serve all read requests. This works because for the majority of web systems there are many more reads than writes.
-
My feeling is that it’s likely that YouYesYet would see more writes than reads. Thus the ‘write to master, read from slaves’ pattern probably isn’t a big win. That isn’t to say that every database master should not have a ‘hot failover’ slave replicating from it which can take over immediately if the master goes down.
-
App servers and database servers
-
The majority of the processing in YouYesYet happens client side; most of what is being sent back to the server is data to be stored directly in the database. So although there will be a small performance win in separating the app server from the database server this isn’t a very big win either.
-
Summary: spreading the load
-
From the above I think the scaling problem should be addressed as follows:
-
-
Start with a pair of database servers (master and hot failover, with replication) and a single app server;
-
Add additional app servers on a load-balancing basis as needed;
-
Add a third database server (‘read server’), also replicating from the master, and direct reads to this;
-
When the initial cluster of three database servers becomes overloaded, shard into two identical groups (‘east’ and ‘west’);
-
When any shard becomes overloaded, split it into two further shards.
-
-
If we have prepared for sharding, all that is required is to duplicate the database.
-
Obviously, once we have split the database into multiple shards, there is a task to integrate the data from the multiple shards in order to create an ‘across Scotland’ overview of the canvas data; however, again if we have prepared for it in advance, merging the databases should not be difficult, and can be done either in the wee sma’ oors or alternatively during the working day, as the system will be relatively lighty loaded during these periods.
-
Preparing for sharding
-
We should prepare a Docker image for the app server and an image or setup script for the database server.
-
## Further reading on optimising Postgres performance
YouYesYet is a proposed web-app intended to simplify the collection of canvas data from voters, specifically for the upcoming Scottish Independence referendum; it is intended that it should later be adaptable for other canvassing campaigns, but that is a much lower priority.
-
General Principles
-
The objective of this is to produce something untrained users can use on a mobile phone on the doorstep on a cold wet night, with potentially intermittent network connectivity.
-
The client side of tha app will be built as a single page app using re-frame. The reason for using a single page, HTML5 app is that additional content does not need to be downloaded on the fly in order to render pages. It should be possible to develop separate Android and iPhone native apps addressing the same back end, but those are not part of the present project. Note that there are still potential issues with intermittent connectivity e.g. for mapping, but elector data should be downloaded in geographical blocks in order that the user doesn’t have to wait on the doorstep of a house for data to download.
-
Classes of Users
-
Canvassers
-
From the point of view of the application, a Canvasser is a user who ‘cold calls’ electors, either by knocking on doors or by telephoning them. All authorised users are deemed to be canvassers.
-
Issue Experts
-
An Issue Expert is a person who is well versed on a particular issue likely to be of concern to electors.
-
It’s my hope that we’d have enough Issue Experts sitting in the comfort of their own homes on a typical canvassing evening that we can get followup phone calls or emails happening within a few minutes of the canvasser calling, so it’s still fresh in the elector’s mind.
-
Analysts
-
Canvassers are able to see voter canvas data for the streets immediately around where they are working. They must be able to do so, because otherwise they cannot effectively canvas. But we must assume that sooner or later a hostile person will join the system as a canvasser, so canvassers should not have access to wider canvas data. Therefore we need a category of trusted user who can view wider maps of where canvas data has been collected, in order to be able to direct teams of canvassers to areas where it has not been collected. For want of a better word I’ll call these Analysts.
-
Administrators
-
An Administrator has access to a full range of back end functions which are not yet fully thought through, but in particular, can
-
-
Lock user accounts
-
Authorise Issue Experts
-
Create new issues
-
-
Creating an account
-
A potential user who does not have a login should be able to create an account. However, not just anybody should be able to create an account, because there is the risk that opponents might register as users and deliberately pollute the database with bad data. Therefore to create an account a user must have an introduction from an existing user - an ‘Invite my friends’ feature of the system should mail out to invitees a token, registered to a particular email address, which allows an account to be created. With that token, creating an account should be simple.
-
My preference is not to store even encrypted passwords of users, but to rely entirely on oauth. This may not be possible but it’s worth a try.
-
So a potential user, who has a token, should be presented with a form in which they are invited to pick a username, supply their real name and phone number, and choose an oauth provider from those we support. They also need to agree to conditions of use. We already have their email address because we know what email address the invitation token was sent to; a confirmation email is sent to this address and the account is not enabled until a link from that email is clicked.
-
Each user record shall contain a field indicating the inviter. Thus, if a user is suspected of acting for the opposition, administrators can see not only who invited that user to join the system, but also which other users have been invited by that user.
-
+------------------------------------------------+
-| Welcome to YouYesYet |
-| |
-| Choose a username: [________________] |
-| Tell us your real name: [________________] |
-| Tell us your phone number: [________________] |
-| Choose an authentication |
-| provider: [Twitter________v] |
-| Agree to the conditions |
-| of use: [I don't agree v] |
-| |
-| [Join us!] |
-+------------------------------------------------+
-
-
Logging in
-
The login screen is very simple: all the user needs enter is their username; they are then directed to their chosen oauth provider to be authenticated.
-
After login
-
After successful login, a Canvasser will be presented with the Map View, see below. All other classes of user will be presented with a menu of their user classes, including the Canvasser user class. Selecting the Canvasser user class will direct them to the Map View. Selecting other user classes will direct to menus of the options available to those user classes.
-
Note that:
-
-
Only the views available to canvassers form part of the one-page app, since it’s assumed that when acting as other user classes the user will have reliable network connectivity;
-
For much the same reason, canvasser views will generally be adapted for use on a mobile phone screen; admin and analyst views may not be.
-
-
Map View
-
The map view shows a map of the streets immediately around their current location, overlaid, on dwellings where canvas has already been done, with icons indicating the voting preference expressed, and with the dwellings where canvassing is still required marked with an icon indicating this:
-
-
Selecting a building on the map leads to
-
-
On buildings with multiple flats, the Building View;
-
On buildings with only one dwelling, the Electors View.
-
-
Building View
-
A list of dwellings in a building.
-
-
Selecting a flat from this view leads to the Electors View.
-
Electors View
-
The Electors View shows a schematic of the registered electors in a dwelling:
-
-
One figure is shown for each elector, labelled with their name. In the dummy pages I’ve shown gendered stick figures, because I believe that in many casesthis will help the canvasser identify the person who has answered the door; but this may be seen as excluding electors with non-binary gender, and, in any case, I believe we don’t actually get gender data (other than salutation) in the electoral roll data. So this may have to be reconsidered.
-
Below the figure are:
-
-
One clear ‘voting intention’ button for each option (e.g., ‘Yes’, ‘No’), greyed unless selected;
-
One issues button.
-
-
Selecting an option icon records that the elector represented by the figure has expressed an intention that they will vote for that option. Selecting the issues icon brings up and issues view.
-
Issues View
-
The Issues View is a simple list of issues:
-
+------------------------------------------------+
-| YouYesYet: Issues [<-] |
-| |
-| Currency |
-| EU Membership |
-| Can Scotland afford it? |
-| Keep the Queen? |
-| Defence/NATO |
-| Other |
-+------------------------------------------------+
-
-
-
This list will not be hard-coded but will be dynamic; thus, if we find an issue we didn’t predict is regularly coming up on the doorstep an Administrator can add it to the list.
-
Selecting the back button from the Issues View returns to the Electors View. Selecting any option from the Issues view leads to the Issue View.
-
Issue View
-
A single page giving top level points the canvasser can make to the elector on the doorstep, regarding the selected issue; and a link to a Followup Request form. There is also a ‘back’ button allowing the user to return to the Issues View.
-
-
Followup Request form
-
The Followup Request form is a simple form which allows the canvasser to record a followup request. The elector and the issue are already known from the route taken to reach the form, so don’t have to be filled in by the user. In case of followup by post (we mail them out a leaflet on the issue) the address is also known. If the elector chooses followup by telephone or by email, the canvasser will be prompted for the telephone number or email address respectively.
Street Canvassers will typically use the system by
-
-
Arriving in an area, often with a group of other canvassers;
-
Logging into the system;
-
Looking at the map view and choosing an uncanvassed dwelling to canvas;
-
Opening the Electors View for that dwelling;
-
Chapping the door;
-
Asking the person who answers about voting intentions, and recording these be selecting the appropriate option buttons;
-
Addressing any issues the elector raises directly, if possible;
-
If there are more detailed issues the elector wants to address, raising a followup request;
-
Making a polite goodbye;
-
Returning to the map view and choosing the next dwelling to canvas.
-
-
Telephone canvassing
-
We also need a workflow for telephone canvassing, but I have not yet given thought to how that would work.
-
Followup Requests view
-
This view is available only to Issue Experts, and is the first view and Issue Expert sees after aelecting Issue Expert from the roles menu.
-
After a canvasser has spoken with an elector, the canvasser may tag the elector with a followup request on an issue. An issue expert will have access to a screen showing all unclosed followup requests for those issues for which he or she is considered an expert. The expert may pick one from the screen, phone or email the elector to discuss the issue, and then either mark the request as closed or add a note for a further followup later.
-
+-------------------------------------------------------------------------------+
-| YouYesYet: Unclosed Followup Requests on Currency |
-| |
-| | Name | Canvasser | Canvassed | Followup by | Contact Detail | |
-| +-------------+-------------+-------------+-------------+-----------------+ |
-| | Archie Bell | Betty Black | 12-Dec-2016 | Telephone | 01312345678 | |
-| | Carol Craig | Donald Dunn | 12-Dec-2016 | eMail | carol@gmail.com | |
-
-
Picking a Followup Request from this list should result in it being temporarily removed from all other Issue Expert’s views, and open the Followup Action view.
-
Followup Action view
-
The Followup Action view shows the name and contact detail of the elector, with the same voting intention buttons as on the Electors View; a toggle to mark this requst closed; a text area to enter any notes; a ‘back’ button which returns to the Followup Requests view; and a list of any previous followup actions on this request with any notes made on them.
-
Below this on the page is a Wiki section which contains links to resources which may be useful in addressing the elector’s concerns.
-
Wiki
-
As specified above, the Followup Action view contains a section which functions as a Wiki page. This may incorporate links to further Wiki pages, or to resources out on the wider Internet. Issue Experts are entitled to edit Wiki pages within the system.
-
Note that the Issue View in the Canvassers’ user interface is a special Wiki page, which can also be edited by the relevant issue experts.
-
Gamification and ‘Scores’
-
Reading up on Bernie Saunders’ campaign’s canvassing app, it apparently contained gamification features which were seen as significantly motivational for younger canvassers. Canvassers should be able to see a screen which shows
-
-
Total time logged in;
-
Total visits made;
-
Total voting intentions recorded;
-
Number of visits made per hour logged in;
-
Number of voting intentions recorded per hour logged in.
-
-
I’d like a way for local canvassing teams to be able to see one another’s scores, so as to encourage friendly competition, but I haven’t yet defined a mechanism for how that might be done. I think a national high-score table would probably be a bad thing, because it might encourage people to create fictional records without actually talking to the electors.
\ No newline at end of file
diff --git a/docs/youyesyet.cache.html b/docs/youyesyet.cache.html
deleted file mode 100644
index cbe470d..0000000
--- a/docs/youyesyet.cache.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-youyesyet.cache documentation
Auto-generated function to select one record from the addresses table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@490531d1. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the authorities table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@352cf516. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the canvassers table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@771caed2. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the canvassers table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@771caed2. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the districts table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@1c48b9f5. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the dwellings table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@f0e6e3d6. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the electors table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@c7611432. Results will be held in cache for 100000 seconds.
Auto-generated function to select one record from the followupmethods table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the followuprequests table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@ae10eaf. Results will be held in cache for 100 seconds.
Auto-generated function to select one record from the genders table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the intentions table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@27b31781. Results will be held in cache for 100 seconds.
Auto-generated function to select one record from the issues table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@606826e4. Results will be held in cache for 1000 seconds.
Auto-generated function to select one record from the options table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the roles table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a81570c6. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the roles table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a81570c6. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the teams table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@31118d64. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the visits table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a7611ce6. Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the addresses table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address :postcode :district_id :latitude :longitude :dwellings :locality :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the authorities table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the canvassers table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised :roles :expertise :teams :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the districts table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the dwellings table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :sub-address :electors :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the electors table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :dwelling_id :phone :email :gender :signature :id). Results will be held in cache for 100000 seconds.
Auto-generated function to select all records from the followupmethods table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the followuprequests table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked :actions :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the genders table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the intentions table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:visit_id :elector_id :option_id :locality :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the issues table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:url :current :brief :experts :id). Results will be held in cache for 1000 seconds.
Auto-generated function to select all records from the options table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the roles table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :members :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the teams table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :district_id :latitude :longitude :members :organisers :events :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the visits table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :canvasser_id :date :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the addresses table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address :postcode :district_id :latitude :longitude :dwellings :locality :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the authorities table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the canvassers table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised :roles :expertise :teams :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the districts table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the dwellings table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :sub-address :electors :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the electors table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :dwelling_id :phone :email :gender :signature :id). Results will be held in cache for 100000 seconds.
Auto-generated function to select all records from the followupmethods table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the followuprequests table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked :actions :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the genders table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the intentions table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:visit_id :elector_id :option_id :locality :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the issues table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:url :current :brief :experts :id). Results will be held in cache for 1000 seconds.
Auto-generated function to select all records from the options table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the roles table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :members :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the teams table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :district_id :latitude :longitude :members :organisers :events :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the visits table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :canvasser_id :date :id). Results will be held in cache for 100 seconds.
On clicking on the pin, navigate to the electors at the address. This way of doing it adds an antry in the browser location history, so back links work.
The default configuration state of the app, when first loaded. 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.
error-details should be a map containing the following keys: :status - error status :title - error title (optional) :message - detailed error message (optional) returns a response map with the error page as the body and the status specified by the status key
Apply this process, assumed to be a function of one argument, to the next item in the queue q, if the queue is not currently locked; return the value returned by process.
Auto-generated function to insert one record to the addresses table. Expects the following key(s) to be present in params: (:address :postcode :district_id :latitude :longitude). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 1000000 seconds.
Auto-generated function to insert one record to the authorities table. Expects the following key(s) to be present in params: (:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000000 seconds.
Auto-generated function to insert one record to the canvassers table. Expects the following key(s) to be present in params: (:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000 seconds.
Auto-generated function to insert one record to the districts table. Expects the following key(s) to be present in params: (:name). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000000 seconds.
Auto-generated function to insert one record to the dwellings table. Expects the following key(s) to be present in params: (:address_id :sub-address). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 1000000 seconds.
Auto-generated function to insert one record to the electors table. Expects the following key(s) to be present in params: (:name :dwelling_id :phone :email :gender :signature). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 100000 seconds.
Auto-generated function to insert one record to the events table. Expects the following key(s) to be present in params: (:name :date :time :decription :cancelled). Returns a map containing the keys #{"id"} identifying the record created.
Auto-generated function to insert one record to the followupactions table. Expects the following key(s) to be present in params: (:request_id :actor :date :notes :closed). Returns a map containing the keys #{"id"} identifying the record created.
Auto-generated function to insert one record to the followupmethods table. Expects the following key(s) to be present in params: (:id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000000 seconds.
Auto-generated function to insert one record to the followuprequests table. Expects the following key(s) to be present in params: (:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 100 seconds.
Auto-generated function to insert one record to the genders table. Expects the following key(s) to be present in params: (:id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 1000000 seconds.
Auto-generated function to insert one record to the intentions table. Expects the following key(s) to be present in params: (:visit_id :elector_id :option_id :locality). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 100 seconds.
Auto-generated function to insert one record to the issues table. Expects the following key(s) to be present in params: (:url :current :brief :id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 1000 seconds.
Auto-generated function to insert one record to the options table. Expects the following key(s) to be present in params: (:id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000000 seconds.
Auto-generated function to insert one record to the roles table. Expects the following key(s) to be present in params: (:name :id). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000000 seconds.
Auto-generated function to insert one record to the teams table. Expects the following key(s) to be present in params: (:name :district_id :latitude :longitude). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 10000 seconds.
Auto-generated function to insert one record to the visits table. Expects the following key(s) to be present in params: (:address_id :canvasser_id :date). Returns a map containing the keys #{"id"} identifying the record created. Results will be held in cache for 100 seconds.
Auto-generated function to delete one record from the addresses table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 1000000 seconds.
Auto-generated function to delete one record from the authorities table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000000 seconds.
Auto-generated function to delete one record from the canvassers table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000 seconds.
Auto-generated function to delete one record from the districts table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000000 seconds.
Auto-generated function to delete one record from the dwellings table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 1000000 seconds.
Auto-generated function to delete one record from the electors table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 100000 seconds.
Auto-generated function to delete one record from the followupmethods table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000000 seconds.
Auto-generated function to delete one record from the followuprequests table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 100 seconds.
Auto-generated function to delete one record from the genders table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 1000000 seconds.
Auto-generated function to delete one record from the intentions table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 100 seconds.
Auto-generated function to delete one record from the issues table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 1000 seconds.
Auto-generated function to delete one record from the options table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000000 seconds.
Auto-generated function to delete one record from the roles table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000000 seconds.
Auto-generated function to delete one record from the teams table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 10000 seconds.
Auto-generated function to delete one record from the visits table. Expects the following key(s) to be present in params: #{"id"}. Results will be held in cache for 100 seconds.
Auto-generated function to select one record from the addresses table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@490531d1. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the authorities table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@352cf516. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the canvassers table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@771caed2. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the canvassers table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@771caed2. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the districts table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@1c48b9f5. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the dwellings table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@f0e6e3d6. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the electors table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@c7611432. Results will be held in cache for 100000 seconds.
Auto-generated function to select one record from the events table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3ac90901.
Auto-generated function to select one record from the followupactions table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@6b32af0e.
Auto-generated function to select one record from the followupmethods table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the followuprequests table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@ae10eaf. Results will be held in cache for 100 seconds.
Auto-generated function to select one record from the genders table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 1000000 seconds.
Auto-generated function to select one record from the intentions table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@27b31781. Results will be held in cache for 100 seconds.
Auto-generated function to select one record from the issues table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@606826e4. Results will be held in cache for 1000 seconds.
Auto-generated function to select one record from the options table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@3c72487c. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the roles table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a81570c6. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the roles table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a81570c6. Results will be held in cache for 10000000 seconds.
Auto-generated function to select one record from the teams table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@31118d64. Results will be held in cache for 10000 seconds.
Auto-generated function to select one record from the visits table. Expects the following key(s) to be present in params: #{"id"}. Returns a map containing the following keys: clojure.lang.LazySeq@a7611ce6. Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the addresses table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address :postcode :district_id :latitude :longitude :dwellings :locality :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the authorities table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the canvassers table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised :roles :expertise :teams :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the districts table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the dwellings table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :sub-address :electors :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the electors table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :dwelling_id :phone :email :gender :signature :id). Results will be held in cache for 100000 seconds.
Auto-generated function to select all records from the events table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :teams :date :time :decription :cancelled :id).
Auto-generated function to select all records from the followupactions table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request_id :actor :date :notes :closed :id).
Auto-generated function to select all records from the followupmethods table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the followuprequests table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked :actions :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the genders table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the intentions table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:visit_id :elector_id :option_id :locality :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the issues table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:url :current :brief :experts :id). Results will be held in cache for 1000 seconds.
Auto-generated function to select all records from the options table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the roles table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :members :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the teams table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :district_id :latitude :longitude :members :organisers :events :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the visits table. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :canvasser_id :date :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the addresses table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address :postcode :district_id :latitude :longitude :dwellings :locality :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the authorities table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the canvassers table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised :roles :expertise :teams :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the districts table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the dwellings table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :sub-address :electors :id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the electors table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :dwelling_id :phone :email :gender :signature :id). Results will be held in cache for 100000 seconds.
Auto-generated function to select all records from the events table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :teams :date :time :decription :cancelled :id).
Auto-generated function to select all records from the followupactions table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:request_id :actor :date :notes :closed :id).
Auto-generated function to select all records from the followupmethods table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the followuprequests table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked :actions :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the genders table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 1000000 seconds.
Auto-generated function to select all records from the intentions table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:visit_id :elector_id :option_id :locality :id). Results will be held in cache for 100 seconds.
Auto-generated function to select all records from the issues table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:url :current :brief :experts :id). Results will be held in cache for 1000 seconds.
Auto-generated function to select all records from the options table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the roles table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :members :id). Results will be held in cache for 10000000 seconds.
Auto-generated function to select all records from the teams table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:name :district_id :latitude :longitude :members :organisers :events :id). Results will be held in cache for 10000 seconds.
Auto-generated function to select all records from the visits table with any text field matching the value of the key :pattern which should be in the request. If the keys (:limit :offset) are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: (:address_id :canvasser_id :date :id). Results will be held in cache for 100 seconds.
Auto-generated function to update one record in the addresses table. Expects the following key(s) to be present in params: (:address :district_id :id :latitude :longitude :postcode). Results will be held in cache for 1000000 seconds.
Auto-generated function to update one record in the authorities table. Expects the following key(s) to be present in params: (:access-token-uri :authorize-uri :consumer-key :consumer-secret :id :request-token-uri). Results will be held in cache for 10000000 seconds.
Auto-generated function to update one record in the canvassers table. Expects the following key(s) to be present in params: (:address_id :authorised :authority_id :avatar :bio :elector_id :email :fullname :id :phone :username). Results will be held in cache for 10000 seconds.
Auto-generated function to update one record in the districts table. Expects the following key(s) to be present in params: (:id :name). Results will be held in cache for 10000000 seconds.
Auto-generated function to update one record in the dwellings table. Expects the following key(s) to be present in params: (:address_id :id :sub-address). Results will be held in cache for 1000000 seconds.
Auto-generated function to update one record in the electors table. Expects the following key(s) to be present in params: (:dwelling_id :email :gender :id :name :phone :signature). Results will be held in cache for 100000 seconds.
Auto-generated function to update one record in the events table. Expects the following key(s) to be present in params: (:cancelled :date :decription :id :name :time).
Auto-generated function to update one record in the followupactions table. Expects the following key(s) to be present in params: (:actor :closed :date :id :notes :request_id).
Auto-generated function to update one record in the followupmethods table. Expects the following key(s) to be present in params: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to update one record in the followuprequests table. Expects the following key(s) to be present in params: (:elector_id :id :issue_detail :issue_id :locked :locked_by :method_detail :method_id :visit_id). Results will be held in cache for 100 seconds.
Auto-generated function to update one record in the genders table. Expects the following key(s) to be present in params: (:id). Results will be held in cache for 1000000 seconds.
Auto-generated function to update one record in the intentions table. Expects the following key(s) to be present in params: (:elector_id :id :locality :option_id :visit_id). Results will be held in cache for 100 seconds.
Auto-generated function to update one record in the issues table. Expects the following key(s) to be present in params: (:brief :current :id :url). Results will be held in cache for 1000 seconds.
Auto-generated function to update one record in the options table. Expects the following key(s) to be present in params: (:id). Results will be held in cache for 10000000 seconds.
Auto-generated function to update one record in the roles table. Expects the following key(s) to be present in params: (:id :name). Results will be held in cache for 10000000 seconds.
Auto-generated function to update one record in the teams table. Expects the following key(s) to be present in params: (:district_id :id :latitude :longitude :name). Results will be held in cache for 10000 seconds.
Auto-generated function to update one record in the visits table. Expects the following key(s) to be present in params: (:address_id :canvasser_id :date :id). Results will be held in cache for 100 seconds.
From this request, create a followupaction record, and, if an option_id is present in the params, an intention record; show the request list on success, the request form on failure.
Manually maintained routes which handle data transfer to/from the canvasser app.
create-intention-and-visit!
(create-intention-and-visit! request)
Doing visit creation logic server side; request params are expected to include an option_id, an elector_id and an address_id, or an option and a location. If no address_id is provided, we simply create an intention record from the option_id and the locality; if an address_id is provided, we need to check whether the last visit by the current user was to the same address, if so use that as the visit_id, if not create a new visit record.
Doing visit creation logic server side; request params are expected to include an issue, an elector_id and an address_id (and also a method_id and method_detail). Ye cannae reasonably create a request without having recorded the visit, so let’s not muck about.
My expectation is that analysts will do a lot of their work through QGIS or some other geographical information system; so there isn’t a need to put anything sophisticated here.
\ No newline at end of file
diff --git a/documentation/authorisation.html b/documentation/authorisation.html
deleted file mode 100644
index 7a7c786..0000000
--- a/documentation/authorisation.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Security and authorisation
Essentially we have six levels of authorisation, at essentially increasing levels of sensitivity.
-
-
-
Canvassers: Any authenticated user essentially has this level of authorisation. Hence users of the app can all share the same database connections without problem. Therefore there will be one first-class database user for all canvassers, and they will not have individual real database logins.
-
-
Issue experts: Issue experts respond to followup requests. Therefore they must be able to see the queue of requests and the details of the elector making the request. They don’t need to see voter intentions and I don’t believe the information they do need to see is particularly sensitive. So they too can share a single database-layer login and connection pool; whether this is the same login as used by the canvassers is an implementation detail but I don’t believe that it’s critical.
-
-
Issue editors: Don’t need to see much sensitive data (although they do need to see, in aggregate, what issues are being raised by electors in the field), but they do have the power to dictate the initial responses canvassers make to issues raised, so the information they can write is pretty sensitive. We need to be very sure that unauthorised users don’t have the power to write this data. So I suggest that issue editors probably should have individual first class database logins.
-
-
Team leaders: Need to be able to monitor the performance of their teams, to invite new users to the system and to block abusive users from the system. Again, these are significant functions which should be well protected from abuse. But we will have at least hundreds, probably thousands of team leaders across Scotland. I would prefer that they each had first class logins, but this may be impractical. But in any case, even if they use a shared login, it should not be the same shared login as used by canvassers.
-
-
Analyists Need broad authorisation to read, but not write or edit, all sensitive data held by the system. They must have individual first class database logins.
-
-
Admins Can necessarily read and write everything. They should definitely each have individual first class database logins.
-
-
This means we have a hybrid authentication scheme; for lower levels, application layer security and shared connection pools are adequate. For higher levels, individual connections and database layer authorisation are required. It implies that the routes at the different layers should be separated into separate namespaces with separate authentication functions.
\ No newline at end of file
diff --git a/documentation/competitors.html b/documentation/competitors.html
deleted file mode 100644
index 22a48d3..0000000
--- a/documentation/competitors.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-Competitor Analysis
Obviously You Yes Yet? is my baby; I’ve put a lot of thought into it. At the time I started working on it I wasn’t aware of any open source competitors; I did to a web search, and I emailed the Bernie Sanders campaign to see whether their widely admired tools were open source. I didn’t find anything.
-
However, I’ve just been pointed to Vote Leave’s Vics tool, and there may well be others.
-
There is no room here for ego. What matters is that the Yes campaign gets the best available tool for the job. So it’s important to do competitor analysis, and not to invest too much work into You Yes Yet? unless there’s a realistic possibility of producing a tool which is better than any of the available alternatives. But it’s also the case that by studying competitors we may find ways to improve the design of You Yes Yet?.
-
Vics
-
Vics, the Voter Intention Collection System, is reputed to have been a significant factor in the successful campaign by Vote Leave to take Britain out of the EU. It has been released as open source under MIT licence, so it is unambiguously available for us to use.
-
The architecture comprises a single-page app built using Angular talking to a server built in Java using the Spring framework. The database engine used is Postgres. Jedis, a Java port of Redis, is used as an in-memory data cache, server side.
-
### Download and initial build
-
I checked out the source from the GitHub repository, and following the instructions in the README created the database and ran a maven install process. Unfortunately, run as a normal user, when this process goes into its test sequence many tests fail unable to contact Jedis. I find it slightly worrying to run such a large and complex build as root, but as root it gets substantially further. The build still doesn’t complete but it seems that it is closer to completion.
-
The ironic point is that it fails because it depends on the JavaScript package manager bower, and bower (very sensibly) refuses to run as root. I therefore made a small modification to the build script to allow it to run bower as root, but unfortunately that didn’t solve the build problem; the jedis service was still not found where it was expected.
-
This is difficult to diagnose; the exception is so deeply nested in framework code that no code from the actual Vics application appears on the stack dump, which makes it very hard to know where to start in debugging.
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
-);
-
\ No newline at end of file
diff --git a/documentation/index.html b/documentation/index.html
deleted file mode 100644
index 30e448f..0000000
--- a/documentation/index.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-Youyesyet 0.2.1
User interface routes for Youyesyet auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at 20180921T143946.559Z
Suppose the YouYesYet project works and we have thousands or tens of thousands of volunteers across Scotland all out chapping doors at the same time: how do we ensure the system stays up under load?
-
Sizing the problem
-
There’s no point in building the app if it will break down under load. We need to be persuaded that it is possible to support the maximum predictable load the system might experience.
-
Database load per volunteer
-
A street canvasser visits on average while working not more than one dwelling every two minutes; the average doorknock-to-doorknock time is probably more like five minutes. Each visit results in
-
-
Zero or one visit record being created;
-
Zero to about five intention records;
-
Zero to about five followup request records.
-
-
So in aggregate minimum zero, maximum about eleven records, typical probably one visit, two intentions = three database inserts per street volunteer per visit. Telephone canvassers probably achieve slightly more because they don’t have to walk from door to door. But over all we’re looking at an average of less than one insert per volunteer per minute.
-
Database reads are probably more infrequent. Each client will obviously need to download the data for each dwelling visited, but it will download these in geograhic blocks of probably around 100 dwellings, and will download a new block only when the user goes outside the area of previously downloaded blocks. However, there ideally should be frequent updates so that the canvasser can see which dwellings other members of the team have already visited, in order that the same dwelling is not visited repeatedly. So there’s probably on average one database read per visit.
-
Reliability of network links
-
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
-
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.
-
Total number of volunteers
-
Personally I’ve never worked in a big canvassing team - maximum about forty people. I believe that there were bigger teams in some parts of urban Scotland. I would guess that the maximum number of volunteers canvassing at any one time - across all groups campaigning for ‘Yes’ in the first independence referendum - never exceeded 35,000 and was probably much lower. I’ve asked whether anyone has better figures but until I have a better estimate I’m going to work on the basis of 35,000 maximum concurrent users.
-
Estimated peak transactions per second
-
This means that the maximum number of transactions per second across Scotland is about
-
35,000 * (1 + 0.2)
------------------- = 700 transactions per second
- 60
-
-
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
-
Caching and memoizing
-
People typically go out canvassing in teams; each member of the team will need the same elector data.
-
Glasgow has a population density of about 3,260 per Km^2; that means each half kilometer square has a maximum population of not much more than 1,000. Downloading 1,000 elector records at startup time is not infeasible. If we normalise data requests to a 100 metre square grid and serve records in 500 metre square chunks, all the members of the same team will request the same chunk of data. Also, elector data is not volatile. Therefore it makes sense to memoize requests for elector data. The app should only request fresh elector data when the device moves within 100 metres of the edge of the current 500 metre cell.
-
Intention data is volatile: we’ll want to update canvassers with fresh intention data frequently, because the other members of their team will be recording intention data as they work, and it’s by seeing that intention data that the canvassers know which doors are still unchapped. So we don’t want to cache intention data for very long. But nevertheless it still makes sense to deliver it in normalised 500 metre square chunks, because that means we can temporarily cache it server side and do not actually have to hit the database with many requests for the same data.
-
Finally, issue data is not volatile over the course of a canvassing session, although it may change over a period of days. So issue data - all the current issues - should be fetched once at app startup time, and not periodically refreshed during a canvassing session. Also, of course, every canvasser will require exactly the same issue data (unless we start thinking of local or regional issues…?), so it absolutely makes sense to memoise requests for issue data.
-
All this normalisation and memoisation reduces the number of read requests on the database.
-
Note that 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.
-
So we could have one database master for each electoral district (or contiguous group of districts) with no real problems except that volunteers working at the very edge of an electoral district would only be supported to canvas on one side of the boundary. I’d rather find an architectural solution which works for the whole of Scotland, but if we cannot do that it isn’t a crisis.
-
It also should not be terribly difficult to organise for a street canvasser user using the Map View to be connected automatically to right geographic shard server, without any action by the user. The issue for telephone canvasser users is a bit different because they will often - perhaps typically - be canvassing voters in a region distant from where they are physically located, so if the geographic sharding model is adopted there would probably have to be an additional electoral district selection screen in the telephone canvasser’s interface.
-
Data from many ‘front-line’ database servers each serving a restricted geographic area can relatively simply be aggregated into a national server by doing the integration work in the wee sma’ oors, when most volunteers (and voters) are asleep.
-
The geographic sharding strategy is scalable. We could start with a single server, split it into a ‘west server’ and an ‘east server’ when that gets overloaded, and further subdivide as needed through the campaign. But we can only do this effectively if we have prepared and tested the strategy in advance.
-
But having considerable numbers of database servers will have cost implications.
-
Geographic sharding by DNS
-
When I first thought of geographic sharding, I intended sharding by electoral district, but actually that makes no sense because electoral districts are complex polygons, which makes point-within-polygon computationally expensive. 4 degrees west falls just west of Stirling, and divides the country in half north-south. 56 degrees north runs north of Edinburgh and Glasgow, but just south of Falkirk. It divides the country in half east to west. Very few towns (and no large towns) straddle either line. Thus we can divide Scotland neatly into four, and it is computationally extremely cheap to compute which shard each data item should be despatched to.
giving us an incredibly simple dispatch table. Furthermore, initially all four addresses can point to the same server. This is an incredibly simple scheme, and I’m confident it’s good enough.
-
Data that’s inserted from the canvassing app - that is to say, voter intention data and followup request data - should have an additional field ‘shard’ (char(2)) which should hold the digraph representing the shard to which it was dispatched, and that field should form part of the primary key, allowing the data from all servers to be integrated. Data that isn’t from the canvassing app should probably be directed to the ‘nw’ shard (which will be lightest loaded), or to a separate master server, and then all servers should be synced overnight.
-
Read servers and write servers
-
It’s a common practice in architecting busy web systems to have one master database server to which all write operations are directed, surrounded by a ring of slave databases which replicate from the master and serve all read requests. This works because for the majority of web systems there are many more reads than writes.
-
My feeling is that it’s likely that YouYesYet would see more writes than reads. Thus the ‘write to master, read from slaves’ pattern probably isn’t a big win. That isn’t to say that every database master should not have a ‘hot failover’ slave replicating from it which can take over immediately if the master goes down.
-
App servers and database servers
-
The majority of the processing in YouYesYet happens client side; most of what is being sent back to the server is data to be stored directly in the database. So although there will be a small performance win in separating the app server from the database server this isn’t a very big win either.
-
Summary: spreading the load
-
From the above I think the scaling problem should be addressed as follows:
-
-
Start with a pair of database servers (master and hot failover, with replication) and a single app server;
-
Add additional app servers on a load-balancing basis as needed;
-
Add a third database server (‘read server’), also replicating from the master, and direct reads to this;
-
When the initial cluster of three database servers becomes overloaded, shard into two identical groups (‘east’ and ‘west’);
-
When any shard becomes overloaded, split it into two further shards.
-
-
If we have prepared for sharding, all that is required is to duplicate the database.
-
Obviously, once we have split the database into multiple shards, there is a task to integrate the data from the multiple shards in order to create an ‘across Scotland’ overview of the canvas data; however, again if we have prepared for it in advance, merging the databases should not be difficult, and can be done either in the wee sma’ oors or alternatively during the working day, as the system will be relatively lighty loaded during these periods.
-
Preparing for sharding
-
We should prepare a Docker image for the app server and an image or setup script for the database server.
-
## Further reading on optimising Postgres performance
YouYesYet is a proposed web-app intended to simplify the collection of canvas data from voters, specifically for the upcoming Scottish Independence referendum; it is intended that it should later be adaptable for other canvassing campaigns, but that is a much lower priority.
-
General Principles
-
The objective of this is to produce something untrained users can use on a mobile phone on the doorstep on a cold wet night, with potentially intermittent network connectivity.
-
The client side of tha app will be built as a single page app using re-frame. The reason for using a single page, HTML5 app is that additional content does not need to be downloaded on the fly in order to render pages. It should be possible to develop separate Android and iPhone native apps addressing the same back end, but those are not part of the present project. Note that there are still potential issues with intermittent connectivity e.g. for mapping, but elector data should be downloaded in geographical blocks in order that the user doesn’t have to wait on the doorstep of a house for data to download.
-
Classes of Users
-
Canvassers
-
From the point of view of the application, a Canvasser is a user who ‘cold calls’ electors, either by knocking on doors or by telephoning them. All authorised users are deemed to be canvassers.
-
Issue Experts
-
An Issue Expert is a person who is well versed on a particular issue likely to be of concern to electors.
-
It’s my hope that we’d have enough Issue Experts sitting in the comfort of their own homes on a typical canvassing evening that we can get followup phone calls or emails happening within a few minutes of the canvasser calling, so it’s still fresh in the elector’s mind.
-
Analysts
-
Canvassers are able to see voter canvas data for the streets immediately around where they are working. They must be able to do so, because otherwise they cannot effectively canvas. But we must assume that sooner or later a hostile person will join the system as a canvasser, so canvassers should not have access to wider canvas data. Therefore we need a category of trusted user who can view wider maps of where canvas data has been collected, in order to be able to direct teams of canvassers to areas where it has not been collected. For want of a better word I’ll call these Analysts.
-
Administrators
-
An Administrator has access to a full range of back end functions which are not yet fully thought through, but in particular, can
-
-
Lock user accounts
-
Authorise Issue Experts
-
Create new issues
-
-
Creating an account
-
A potential user who does not have a login should be able to create an account. However, not just anybody should be able to create an account, because there is the risk that opponents might register as users and deliberately pollute the database with bad data. Therefore to create an account a user must have an introduction from an existing user - an ‘Invite my friends’ feature of the system should mail out to invitees a token, registered to a particular email address, which allows an account to be created. With that token, creating an account should be simple.
-
My preference is not to store even encrypted passwords of users, but to rely entirely on oauth. This may not be possible but it’s worth a try.
-
So a potential user, who has a token, should be presented with a form in which they are invited to pick a username, supply their real name and phone number, and choose an oauth provider from those we support. They also need to agree to conditions of use. We already have their email address because we know what email address the invitation token was sent to; a confirmation email is sent to this address and the account is not enabled until a link from that email is clicked.
-
Each user record shall contain a field indicating the inviter. Thus, if a user is suspected of acting for the opposition, administrators can see not only who invited that user to join the system, but also which other users have been invited by that user.
-
+------------------------------------------------+
-| Welcome to YouYesYet |
-| |
-| Choose a username: [________________] |
-| Tell us your real name: [________________] |
-| Tell us your phone number: [________________] |
-| Choose an authentication |
-| provider: [Twitter________v] |
-| Agree to the conditions |
-| of use: [I don't agree v] |
-| |
-| [Join us!] |
-+------------------------------------------------+
-
-
Logging in
-
The login screen is very simple: all the user needs enter is their username; they are then directed to their chosen oauth provider to be authenticated.
-
After login
-
After successful login, a Canvasser will be presented with the Map View, see below. All other classes of user will be presented with a menu of their user classes, including the Canvasser user class. Selecting the Canvasser user class will direct them to the Map View. Selecting other user classes will direct to menus of the options available to those user classes.
-
Note that:
-
-
Only the views available to canvassers form part of the one-page app, since it’s assumed that when acting as other user classes the user will have reliable network connectivity;
-
For much the same reason, canvasser views will generally be adapted for use on a mobile phone screen; admin and analyst views may not be.
-
-
Map View
-
The map view shows a map of the streets immediately around their current location, overlaid, on dwellings where canvas has already been done, with icons indicating the voting preference expressed, and with the dwellings where canvassing is still required marked with an icon indicating this:
-
-
Selecting a building on the map leads to
-
-
On buildings with multiple flats, the Building View;
-
On buildings with only one dwelling, the Electors View.
-
-
Building View
-
A list of dwellings in a building.
-
-
Selecting a flat from this view leads to the Electors View.
-
Electors View
-
The Electors View shows a schematic of the registered electors in a dwelling:
-
-
One figure is shown for each elector, labelled with their name. In the dummy pages I’ve shown gendered stick figures, because I believe that in many casesthis will help the canvasser identify the person who has answered the door; but this may be seen as excluding electors with non-binary gender, and, in any case, I believe we don’t actually get gender data (other than salutation) in the electoral roll data. So this may have to be reconsidered.
-
Below the figure are:
-
-
One clear ‘voting intention’ button for each option (e.g., ‘Yes’, ‘No’), greyed unless selected;
-
One issues button.
-
-
Selecting an option icon records that the elector represented by the figure has expressed an intention that they will vote for that option. Selecting the issues icon brings up and issues view.
-
Issues View
-
The Issues View is a simple list of issues:
-
+------------------------------------------------+
-| YouYesYet: Issues [<-] |
-| |
-| Currency |
-| EU Membership |
-| Can Scotland afford it? |
-| Keep the Queen? |
-| Defence/NATO |
-| Other |
-+------------------------------------------------+
-
-
-
This list will not be hard-coded but will be dynamic; thus, if we find an issue we didn’t predict is regularly coming up on the doorstep an Administrator can add it to the list.
-
Selecting the back button from the Issues View returns to the Electors View. Selecting any option from the Issues view leads to the Issue View.
-
Issue View
-
A single page giving top level points the canvasser can make to the elector on the doorstep, regarding the selected issue; and a link to a Followup Request form. There is also a ‘back’ button allowing the user to return to the Issues View.
-
-
Followup Request form
-
The Followup Request form is a simple form which allows the canvasser to record a followup request. The elector and the issue are already known from the route taken to reach the form, so don’t have to be filled in by the user. In case of followup by post (we mail them out a leaflet on the issue) the address is also known. If the elector chooses followup by telephone or by email, the canvasser will be prompted for the telephone number or email address respectively.
Street Canvassers will typically use the system by
-
-
Arriving in an area, often with a group of other canvassers;
-
Logging into the system;
-
Looking at the map view and choosing an uncanvassed dwelling to canvas;
-
Opening the Electors View for that dwelling;
-
Chapping the door;
-
Asking the person who answers about voting intentions, and recording these be selecting the appropriate option buttons;
-
Addressing any issues the elector raises directly, if possible;
-
If there are more detailed issues the elector wants to address, raising a followup request;
-
Making a polite goodbye;
-
Returning to the map view and choosing the next dwelling to canvas.
-
-
Telephone canvassing
-
We also need a workflow for telephone canvassing, but I have not yet given thought to how that would work.
-
Followup Requests view
-
This view is available only to Issue Experts, and is the first view and Issue Expert sees after aelecting Issue Expert from the roles menu.
-
After a canvasser has spoken with an elector, the canvasser may tag the elector with a followup request on an issue. An issue expert will have access to a screen showing all unclosed followup requests for those issues for which he or she is considered an expert. The expert may pick one from the screen, phone or email the elector to discuss the issue, and then either mark the request as closed or add a note for a further followup later.
-
+-------------------------------------------------------------------------------+
-| YouYesYet: Unclosed Followup Requests on Currency |
-| |
-| | Name | Canvasser | Canvassed | Followup by | Contact Detail | |
-| +-------------+-------------+-------------+-------------+-----------------+ |
-| | Archie Bell | Betty Black | 12-Dec-2016 | Telephone | 01312345678 | |
-| | Carol Craig | Donald Dunn | 12-Dec-2016 | eMail | carol@gmail.com | |
-
-
Picking a Followup Request from this list should result in it being temporarily removed from all other Issue Expert’s views, and open the Followup Action view.
-
Followup Action view
-
The Followup Action view shows the name and contact detail of the elector, with the same voting intention buttons as on the Electors View; a toggle to mark this requst closed; a text area to enter any notes; a ‘back’ button which returns to the Followup Requests view; and a list of any previous followup actions on this request with any notes made on them.
-
Below this on the page is a Wiki section which contains links to resources which may be useful in addressing the elector’s concerns.
-
Wiki
-
As specified above, the Followup Action view contains a section which functions as a Wiki page. This may incorporate links to further Wiki pages, or to resources out on the wider Internet. Issue Experts are entitled to edit Wiki pages within the system.
-
Note that the Issue View in the Canvassers’ user interface is a special Wiki page, which can also be edited by the relevant issue experts.
-
Gamification and ‘Scores’
-
Reading up on Bernie Saunders’ campaign’s canvassing app, it apparently contained gamification features which were seen as significantly motivational for younger canvassers. Canvassers should be able to see a screen which shows
-
-
Total time logged in;
-
Total visits made;
-
Total voting intentions recorded;
-
Number of visits made per hour logged in;
-
Number of voting intentions recorded per hour logged in.
-
-
I’d like a way for local canvassing teams to be able to see one another’s scores, so as to encourage friendly competition, but I haven’t yet defined a mechanism for how that might be done. I think a national high-score table would probably be a bad thing, because it might encourage people to create fictional records without actually talking to the electors.
\ No newline at end of file
diff --git a/documentation/youyesyet.authorisation.html b/documentation/youyesyet.authorisation.html
deleted file mode 100644
index 65b4825..0000000
--- a/documentation/youyesyet.authorisation.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
-youyesyet.authorisation documentation
Render web pages using Selmer templating markup.
-
*app-context*
dynamic
FIXME: write docs
*user*
dynamic
FIXME: write docs
error-page
(error-page error-details)
error-details should be a map containing the following keys:
-:status - error status
-:title - error title (optional)
-:message - detailed error message (optional)
-returns a response map with the error page as the body
-and the status specified by the status key
get-user-roles
Return, as a set, the names of the roles of which this `user` is a member.
-
render
(render template & [params])
renders the HTML `template` located relative to resources/templates in
-the context of this session and with these parameters.
render-with-session
(render-with-session template session & [params])
renders the HTML `template` located relative to resources/templates in
-the context of this session and with these parameters.
\ No newline at end of file
diff --git a/documentation/youyesyet.locality.html b/documentation/youyesyet.locality.html
deleted file mode 100644
index dd328e7..0000000
--- a/documentation/youyesyet.locality.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-youyesyet.locality documentation
Queue of messages waiting to be sent to the server.
-
add!
(add! q item)
Add this item to the queue.
-
count
(count q)
Return the count of items currently in the queue.
-
lock!
(lock! q)
FIXME: write docs
locked?
(locked? q)
FIXME: write docs
maybe-process-next
(maybe-process-next q process)
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.
new-queue
(new-queue)(new-queue items)
Create a new queue
-
peek
(peek q)
Look at the next item which could be removed from the queue.
-
queue?
(queue? x)
True if x is a queue, else false.
-
take!
(take! q)
Return the first item from the queue, rebind the queue to the remaining
-items. If the queue is empty return nil.
unlock!
(unlock! q)(unlock! q value)
FIXME: write docs
\ No newline at end of file
diff --git a/documentation/youyesyet.routes.auto-json.html b/documentation/youyesyet.routes.auto-json.html
deleted file mode 100644
index a0f6943..0000000
--- a/documentation/youyesyet.routes.auto-json.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-youyesyet.routes.auto-json documentation
JSON routes for youyesyet auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at 20180921T143945.111Z
-
auto-rest-routes
FIXME: write docs
create-address!
(create-address! request)
Auto-generated method to insert one record to the `addresses` table. Expects the following key(s) to be present in `params`: `(:address :postcode :district_id :latitude :longitude)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-authority!
(create-authority! request)
Auto-generated method to insert one record to the `authorities` table. Expects the following key(s) to be present in `params`: `(:request-token-uri :access-token-uri :authorize-uri :consumer-key :consumer-secret :id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-canvasser!
(create-canvasser! request)
Auto-generated method to insert one record to the `canvassers` table. Expects the following key(s) to be present in `params`: `(:username :fullname :avatar :bio :elector_id :address_id :phone :email :authority_id :authorised)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-district!
(create-district! request)
Auto-generated method to insert one record to the `districts` table. Expects the following key(s) to be present in `params`: `(:name)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-dwelling!
(create-dwelling! request)
Auto-generated method to insert one record to the `dwellings` table. Expects the following key(s) to be present in `params`: `(:address_id :sub-address)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-elector!
(create-elector! request)
Auto-generated method to insert one record to the `electors` table. Expects the following key(s) to be present in `params`: `(:name :dwelling_id :phone :email :gender :signature)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-event!
(create-event! request)
Auto-generated method to insert one record to the `events` table. Expects the following key(s) to be present in `params`: `(:name :date :time :decription :cancelled)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-followupaction!
(create-followupaction! request)
Auto-generated method to insert one record to the `followupactions` table. Expects the following key(s) to be present in `params`: `(:request_id :actor :date :notes :closed)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-followupmethod!
(create-followupmethod! request)
Auto-generated method to insert one record to the `followupmethods` table. Expects the following key(s) to be present in `params`: `(:id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-followuprequest!
(create-followuprequest! request)
Auto-generated method to insert one record to the `followuprequests` table. Expects the following key(s) to be present in `params`: `(:elector_id :visit_id :issue_id :issue_detail :method_id :method_detail :locked_by :locked)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-gender!
(create-gender! request)
Auto-generated method to insert one record to the `genders` table. Expects the following key(s) to be present in `params`: `(:id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-intention!
(create-intention! request)
Auto-generated method to insert one record to the `intentions` table. Expects the following key(s) to be present in `params`: `(:visit_id :elector_id :option_id :locality)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-issue!
(create-issue! request)
Auto-generated method to insert one record to the `issues` table. Expects the following key(s) to be present in `params`: `(:url :current :brief :id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-option!
(create-option! request)
Auto-generated method to insert one record to the `options` table. Expects the following key(s) to be present in `params`: `(:id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-role!
(create-role! request)
Auto-generated method to insert one record to the `roles` table. Expects the following key(s) to be present in `params`: `(:name :id)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-team!
(create-team! request)
Auto-generated method to insert one record to the `teams` table. Expects the following key(s) to be present in `params`: `(:name :district_id :latitude :longitude)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
create-visit!
(create-visit! request)
Auto-generated method to insert one record to the `visits` table. Expects the following key(s) to be present in `params`: `(:address_id :canvasser_id :date)`. Returns a map containing the keys `#{"id"}` identifying the record created.
-
delete-address!
(delete-address! request)
Auto-generated method to delete one record from the `addresses` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-authority!
(delete-authority! request)
Auto-generated method to delete one record from the `authorities` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-canvasser!
(delete-canvasser! request)
Auto-generated method to delete one record from the `canvassers` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-district!
(delete-district! request)
Auto-generated method to delete one record from the `districts` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-dwelling!
(delete-dwelling! request)
Auto-generated method to delete one record from the `dwellings` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-elector!
(delete-elector! request)
Auto-generated method to delete one record from the `electors` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-event!
(delete-event! request)
Auto-generated method to delete one record from the `events` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-followupaction!
(delete-followupaction! request)
Auto-generated method to delete one record from the `followupactions` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-followupmethod!
(delete-followupmethod! request)
Auto-generated method to delete one record from the `followupmethods` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-followuprequest!
(delete-followuprequest! request)
Auto-generated method to delete one record from the `followuprequests` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-gender!
(delete-gender! request)
Auto-generated method to delete one record from the `genders` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-intention!
(delete-intention! request)
Auto-generated method to delete one record from the `intentions` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-issue!
(delete-issue! request)
Auto-generated method to delete one record from the `issues` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-option!
(delete-option! request)
Auto-generated method to delete one record from the `options` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-role!
(delete-role! request)
Auto-generated method to delete one record from the `roles` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-team!
(delete-team! request)
Auto-generated method to delete one record from the `teams` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
delete-visit!
(delete-visit! request)
Auto-generated method to delete one record from the `visits` table. Expects the following key(s) to be present in `params`: `#{"id"}`.
-
get-address
FIXME: write docs
get-authority
FIXME: write docs
get-canvasser
FIXME: write docs
get-canvasser-by-username
FIXME: write docs
get-district
FIXME: write docs
get-dwelling
FIXME: write docs
get-elector
FIXME: write docs
get-event
(get-event request)
Auto-generated method to select one record from the `events` table. Expects the following key(s) to be present in `params`: `#{"id"}`. Returns a map containing the following keys: `clojure.lang.LazySeq@3ac90901`.
-
get-followupaction
(get-followupaction request)
Auto-generated method to select one record from the `followupactions` table. Expects the following key(s) to be present in `params`: `#{"id"}`. Returns a map containing the following keys: `clojure.lang.LazySeq@6b32af0e`.
-
get-followupmethod
FIXME: write docs
get-followuprequest
FIXME: write docs
get-gender
FIXME: write docs
get-intention
FIXME: write docs
get-issue
FIXME: write docs
get-option
FIXME: write docs
get-role
FIXME: write docs
get-role-by-name
FIXME: write docs
get-team
FIXME: write docs
get-visit
FIXME: write docs
list-addresses
FIXME: write docs
list-addresses-by-district
(list-addresses-by-district {:keys [params]})
FIXME: write docs
list-authorities
FIXME: write docs
list-canvassers
FIXME: write docs
list-canvassers-by-address
(list-canvassers-by-address {:keys [params]})
FIXME: write docs
list-canvassers-by-authority
(list-canvassers-by-authority {:keys [params]})
FIXME: write docs
list-canvassers-by-elector
(list-canvassers-by-elector {:keys [params]})
FIXME: write docs
list-canvassers-by-issues
(list-canvassers-by-issues {:keys [params]})
FIXME: write docs
list-canvassers-by-roles
(list-canvassers-by-roles {:keys [params]})
FIXME: write docs
list-districts
FIXME: write docs
list-dwellings
FIXME: write docs
list-dwellings-by-address
(list-dwellings-by-address {:keys [params]})
FIXME: write docs
list-electors
FIXME: write docs
list-electors-by-dwelling
(list-electors-by-dwelling {:keys [params]})
FIXME: write docs
list-electors-by-gender
(list-electors-by-gender {:keys [params]})
FIXME: write docs
list-events
(list-events request)
Auto-generated method to select all records from the `events` table. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `(:name :teams :date :time :decription :cancelled :id)`.
-
list-events-by-teams
(list-events-by-teams {:keys [params]})
FIXME: write docs
list-followupactions
(list-followupactions request)
Auto-generated method to select all records from the `followupactions` table. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `(:request_id :actor :date :notes :closed :id)`.
-
Auto-generated method to select all records from the `events` table with any text field matching the value of the key `:pattern` which should be in the request. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `(:name :teams :date :time :decription :cancelled :id)`.
-
search-strings-followupactions
(search-strings-followupactions request)
Auto-generated method to select all records from the `followupactions` table with any text field matching the value of the key `:pattern` which should be in the request. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `(:request_id :actor :date :notes :closed :id)`.
-
search-strings-followupmethods
FIXME: write docs
search-strings-followuprequests
FIXME: write docs
search-strings-genders
FIXME: write docs
search-strings-intentions
FIXME: write docs
search-strings-issues
FIXME: write docs
search-strings-options
FIXME: write docs
search-strings-roles
FIXME: write docs
search-strings-teams
FIXME: write docs
search-strings-visits
FIXME: write docs
update-address!
(update-address! request)
Auto-generated method to update one record in the `addresses` table. Expects the following key(s) to be present in `params`: `(:address :district_id :id :latitude :longitude :postcode)`.
-
update-authority!
(update-authority! request)
Auto-generated method to update one record in the `authorities` table. Expects the following key(s) to be present in `params`: `(:access-token-uri :authorize-uri :consumer-key :consumer-secret :id :request-token-uri)`.
-
update-canvasser!
(update-canvasser! request)
Auto-generated method to update one record in the `canvassers` table. Expects the following key(s) to be present in `params`: `(:address_id :authorised :authority_id :avatar :bio :elector_id :email :fullname :id :phone :username)`.
-
update-district!
(update-district! request)
Auto-generated method to update one record in the `districts` table. Expects the following key(s) to be present in `params`: `(:id :name)`.
-
update-dwelling!
(update-dwelling! request)
Auto-generated method to update one record in the `dwellings` table. Expects the following key(s) to be present in `params`: `(:address_id :id :sub-address)`.
-
update-elector!
(update-elector! request)
Auto-generated method to update one record in the `electors` table. Expects the following key(s) to be present in `params`: `(:dwelling_id :email :gender :id :name :phone :signature)`.
-
update-event!
(update-event! request)
Auto-generated method to update one record in the `events` table. Expects the following key(s) to be present in `params`: `(:cancelled :date :decription :id :name :time)`.
-
update-followupaction!
(update-followupaction! request)
Auto-generated method to update one record in the `followupactions` table. Expects the following key(s) to be present in `params`: `(:actor :closed :date :id :notes :request_id)`.
-
update-followupmethod!
(update-followupmethod! request)
Auto-generated method to update one record in the `followupmethods` table. Expects the following key(s) to be present in `params`: `(:id)`.
-
update-followuprequest!
(update-followuprequest! request)
Auto-generated method to update one record in the `followuprequests` table. Expects the following key(s) to be present in `params`: `(:elector_id :id :issue_detail :issue_id :locked :locked_by :method_detail :method_id :visit_id)`.
-
update-gender!
(update-gender! request)
Auto-generated method to update one record in the `genders` table. Expects the following key(s) to be present in `params`: `(:id)`.
-
update-intention!
(update-intention! request)
Auto-generated method to update one record in the `intentions` table. Expects the following key(s) to be present in `params`: `(:elector_id :id :locality :option_id :visit_id)`.
-
update-issue!
(update-issue! request)
Auto-generated method to update one record in the `issues` table. Expects the following key(s) to be present in `params`: `(:brief :current :id :url)`.
-
update-option!
(update-option! request)
Auto-generated method to update one record in the `options` table. Expects the following key(s) to be present in `params`: `(:id)`.
-
update-role!
(update-role! request)
Auto-generated method to update one record in the `roles` table. Expects the following key(s) to be present in `params`: `(:id :name)`.
-
update-team!
(update-team! request)
Auto-generated method to update one record in the `teams` table. Expects the following key(s) to be present in `params`: `(:district_id :id :latitude :longitude :name)`.
-
update-visit!
(update-visit! request)
Auto-generated method to update one record in the `visits` table. Expects the following key(s) to be present in `params`: `(:address_id :canvasser_id :date :id)`.
-
\ No newline at end of file
diff --git a/documentation/youyesyet.routes.auto.html b/documentation/youyesyet.routes.auto.html
deleted file mode 100644
index 5ab54ce..0000000
--- a/documentation/youyesyet.routes.auto.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
-youyesyet.routes.auto documentation
User interface routes for Youyesyet auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at 20180921T143946.559Z
-
Return the `followuprequest` record indicated by this `id`, provided that
-it is unlocked. As a side effect, lock it to this `user`.
get-followup-request-page
(get-followup-request-page request)
FIXME: write docs
issue-expert-routes
FIXME: write docs
list-page
(list-page request)
FIXME: write docs
post-followup-action
(post-followup-action request)
From this `request`, create a `followupaction` record, and, if an
-`option_id` is present in the params, an `intention` record; show
-the request list on success, the request form on failure.
release-followuprequest!
(release-followuprequest! id user)
Release the lock held on the `followuprequest` record indicated by this
-`id` held by this `user`, if present.
\ No newline at end of file
diff --git a/documentation/youyesyet.routes.manual.html b/documentation/youyesyet.routes.manual.html
deleted file mode 100644
index 7d1a732..0000000
--- a/documentation/youyesyet.routes.manual.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
-youyesyet.routes.manual documentation
Manually maintained routes which handle data transfer to/from the canvasser app.
-
create-intention-and-visit!
(create-intention-and-visit! request)
Doing visit creation logic server side; request params are expected to
-include an `option_id`, an `elector_id` and an `address_id`, or an `option` and
-a `location`. If no `address_id` is provided, we simply create an
-`intention` record from the `option_id` and the `locality`; if an `address_id`
-is provided, we need to check whether the last `visit` by the current `user`
-was to the same address, if so use that as the `visit_id`, if not create
-a new `visit` record.
create-request-and-visit!
(create-request-and-visit! request)
Doing visit creation logic server side; request params are expected to
-include an `issue`, an `elector_id` and an `address_id` (and also a
-`method_id` and `method_detail`). Ye cannae reasonably create a request
-without having recorded the visit, so let's not muck about.
current-visit-id
(current-visit-id request)
Return the id of the current visit by the current user, creating it if necessary.
-
get-local-data
(get-local-data request)
Get data local to the user of the canvasser app. Expects arguments `latitude` and
-`longitude`, or `locality`. Returns a block of data for that locality
in-get-local-data
Local data is volatile, because hopefully canvassers are updating it as they
-work. So cache for only 90 seconds.
last-visit-by-current-user
(last-visit-by-current-user request)
Return the most recent visit by the currently logged in user
-
rest-routes
FIXME: write docs
update-elector-signature!
(update-elector-signature! request)
Set the `signature` in the params of this `request` as the signature for
-the elector whose `id` is in the params of this `request`.
\ No newline at end of file
diff --git a/documentation/youyesyet.routes.roles.html b/documentation/youyesyet.routes.roles.html
deleted file mode 100644
index 5cd69b6..0000000
--- a/documentation/youyesyet.routes.roles.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-youyesyet.routes.roles documentation
Routes/pages available to authenticated users in specific roles.
-
admins-page
(admins-page request)
FIXME: write docs
analysts-page
(analysts-page request)
My expectation is that analysts will do a lot of their work through QGIS or
-some other geographical information system; so there isn't a need to put
-anything sophisticated here.
canvassers-page
(canvassers-page request)
FIXME: write docs
roles-page
(roles-page request)
FIXME: write docs
roles-routes
FIXME: write docs
team-organisers-page
(team-organisers-page request)
FIXME: write docs
\ No newline at end of file
diff --git a/documentation/youyesyet.routes.services.html b/documentation/youyesyet.routes.services.html
deleted file mode 100644
index 87024de..0000000
--- a/documentation/youyesyet.routes.services.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
-youyesyet.routes.services documentation
\ No newline at end of file
diff --git a/dummies/building.png b/dummies/building.png
deleted file mode 100644
index ff8fbc3..0000000
Binary files a/dummies/building.png and /dev/null differ
diff --git a/dummies/building.xcf b/dummies/building.xcf
deleted file mode 100644
index 4c494bb..0000000
Binary files a/dummies/building.xcf and /dev/null differ
diff --git a/dummies/followup.png b/dummies/followup.png
deleted file mode 100644
index 768a3d0..0000000
Binary files a/dummies/followup.png and /dev/null differ
diff --git a/dummies/followup.xcf b/dummies/followup.xcf
deleted file mode 100644
index 36db36e..0000000
Binary files a/dummies/followup.xcf and /dev/null differ
diff --git a/dummies/issue.png b/dummies/issue.png
deleted file mode 100644
index 7591d5e..0000000
Binary files a/dummies/issue.png and /dev/null differ
diff --git a/dummies/issue.xcf b/dummies/issue.xcf
deleted file mode 100644
index 0498dd5..0000000
Binary files a/dummies/issue.xcf and /dev/null differ
diff --git a/dummies/issues.png b/dummies/issues.png
deleted file mode 100644
index 1249700..0000000
Binary files a/dummies/issues.png and /dev/null differ
diff --git a/dummies/issues.xcf b/dummies/issues.xcf
deleted file mode 100644
index e6ae96f..0000000
Binary files a/dummies/issues.xcf and /dev/null differ
diff --git a/dummies/mapview.png b/dummies/mapview.png
new file mode 100644
index 0000000..1fe15e1
Binary files /dev/null and b/dummies/mapview.png differ
diff --git a/dummies/mapview.svg b/dummies/mapview.svg
new file mode 100644
index 0000000..c909b17
--- /dev/null
+++ b/dummies/mapview.svg
@@ -0,0 +1,4874 @@
+
+
+
+
diff --git a/dummies/mapview.xcf b/dummies/mapview.xcf
new file mode 100644
index 0000000..cf643e1
Binary files /dev/null and b/dummies/mapview.xcf differ
diff --git a/dummies/mapview_800.png b/dummies/mapview_800.png
new file mode 100644
index 0000000..5173125
Binary files /dev/null and b/dummies/mapview_800.png differ
diff --git a/dummies/mapview_800.xcf b/dummies/mapview_800.xcf
new file mode 100644
index 0000000..47d5ccf
Binary files /dev/null and b/dummies/mapview_800.xcf differ
diff --git a/dummies/occupants.png b/dummies/occupants.png
new file mode 100644
index 0000000..e1a29ca
Binary files /dev/null and b/dummies/occupants.png differ
diff --git a/dummies/occupants.svg b/dummies/occupants.svg
new file mode 100644
index 0000000..ed4486c
--- /dev/null
+++ b/dummies/occupants.svg
@@ -0,0 +1,454 @@
+
+
+
+
diff --git a/dummies/occupants.xcf b/dummies/occupants.xcf
new file mode 100644
index 0000000..c584d9a
Binary files /dev/null and b/dummies/occupants.xcf differ
diff --git a/dummies/occupants_800.png b/dummies/occupants_800.png
new file mode 100644
index 0000000..70b0d9f
Binary files /dev/null and b/dummies/occupants_800.png differ
diff --git a/dummies/occupants_800.xcf b/dummies/occupants_800.xcf
new file mode 100644
index 0000000..eb7c497
Binary files /dev/null and b/dummies/occupants_800.xcf differ
diff --git a/dummies/ujack.png b/dummies/ujack.png
new file mode 100644
index 0000000..d542490
Binary files /dev/null and b/dummies/ujack.png differ
diff --git a/dummies/ujack.xcf b/dummies/ujack.xcf
new file mode 100644
index 0000000..a02e5df
Binary files /dev/null and b/dummies/ujack.xcf differ
diff --git a/dummies/unknown.png b/dummies/unknown.png
new file mode 100644
index 0000000..da3ac5b
Binary files /dev/null and b/dummies/unknown.png differ
diff --git a/dummies/unknown.xcf b/dummies/unknown.xcf
new file mode 100644
index 0000000..b1806e5
Binary files /dev/null and b/dummies/unknown.xcf differ
diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj
index 97cb362..a8adafd 100644
--- a/env/dev/clj/user.clj
+++ b/env/dev/clj/user.clj
@@ -13,23 +13,4 @@
(stop)
(start))
-;; Roughly working under Tomcat.
-;; Database setup using JNDI: see http://www.luminusweb.net/docs/deployment.md#deploying_to_tomcat
-;; Note that this duplicates configuration in profiles.clj; one of these is wrong (and neither
-;; should be in the Git repository but this is for now!)
-(System/setProperty "java.naming.factory.initial"
- "org.apache.naming.java.javaURLContextFactory")
-(System/setProperty "java.naming.factory.url.pkgs"
- "org.apache.naming")
-(doto (new javax.naming.InitialContext)
- (.createSubcontext "java:")
- (.createSubcontext "java:comp")
- (.createSubcontext "java:comp/env")
- (.createSubcontext "java:comp/env/jdbc")
- (.bind "java:comp/env/jdbc/testdb"
- (doto (org.postgresql.ds.PGSimpleDataSource.)
- (.setServerName "localhost")
- (.setDatabaseName "youyesyet_dev")
- (.setUser "youyesyet")
- (.setPassword "thisisnotsecure"))))
diff --git a/env/dev/clj/youyesyet/core.clj b/env/dev/clj/youyesyet/core.clj
index 0a287bb..1b703a4 100644
--- a/env/dev/clj/youyesyet/core.clj
+++ b/env/dev/clj/youyesyet/core.clj
@@ -1,27 +1,18 @@
-(ns ^{:doc "Development launcher, entirely boilerplate from Luminus."}
- youyesyet.core
- (:require [clojure.tools.cli :refer [parse-opts]]
- [clojure.tools.logging :as log]
+(ns youyesyet.core
+ (:require [youyesyet.handler :as handler]
[luminus.repl-server :as repl]
[luminus.http-server :as http]
[luminus-migrations.core :as migrations]
- [mount.core :as mount]
[youyesyet.config :refer [env]]
- [youyesyet.handler :as handler])
+ [clojure.tools.cli :refer [parse-opts]]
+ [clojure.tools.logging :as log]
+ [mount.core :as mount])
(:gen-class))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;
-;;;; youyesyet.core: run as a standalone application. Entirely luminus
-;;;; boilerplate.
-;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
(def cli-options
[["-p" "--port PORT" "Port number"
:parse-fn #(Integer/parseInt %)]])
-
(mount/defstate ^{:on-reload :noop}
http-server
:start
@@ -32,7 +23,6 @@
:stop
(http/stop http-server))
-
(mount/defstate ^{:on-reload :noop}
repl-server
:start
@@ -49,7 +39,6 @@
(System/setProperty "java.naming.factory.url.pkgs"
"org.apache.naming"))
-
(defn start-app [args]
(init-jndi)
(doseq [component (-> args
@@ -57,9 +46,7 @@
mount/start-with-args
:started)]
(log/info component "started"))
- (.addShutdownHook (Runtime/getRuntime)
- (Thread. handler/destroy)))
-
+ (.addShutdownHook (Runtime/getRuntime) (Thread. handler/destroy)))
(defn -main [& args]
(cond
@@ -70,9 +57,4 @@
(System/exit 0))
:else
(start-app args)))
-
-;; (mount/start)
-;; (mount/stop)
-
-
-
+
diff --git a/env/dev/clj/youyesyet/dev_middleware.clj b/env/dev/clj/youyesyet/dev_middleware.clj
index 23fd9db..d64d5d5 100644
--- a/env/dev/clj/youyesyet/dev_middleware.clj
+++ b/env/dev/clj/youyesyet/dev_middleware.clj
@@ -1,13 +1,10 @@
(ns youyesyet.dev-middleware
- (:require
-;; [ring.middleware.reload :refer [wrap-reload]] ;; this fails with a self referential dependency, which I haven't tracked down.
+ (:require [ring.middleware.reload :refer [wrap-reload]]
[selmer.middleware :refer [wrap-error-page]]
- [prone.middleware :refer [wrap-exceptions]]
- ))
+ [prone.middleware :refer [wrap-exceptions]]))
(defn wrap-dev [handler]
- (-> handler
-;; wrap-reload
- wrap-error-page
- wrap-exceptions
- ))
+ (-> handler
+ wrap-reload
+ wrap-error-page
+ wrap-exceptions))
diff --git a/env/dev/clj/youyesyet/env.clj b/env/dev/clj/youyesyet/env.clj
index ef35fd0..87bfa55 100644
--- a/env/dev/clj/youyesyet/env.clj
+++ b/env/dev/clj/youyesyet/env.clj
@@ -1,8 +1,7 @@
(ns youyesyet.env
(:require [selmer.parser :as parser]
[clojure.tools.logging :as log]
- [youyesyet.dev-middleware :refer [wrap-dev]]
- ))
+ [youyesyet.dev-middleware :refer [wrap-dev]]))
(def defaults
{:init
@@ -12,5 +11,4 @@
:stop
(fn []
(log/info "\n-=[youyesyet has shut down successfully]=-"))
- :middleware wrap-dev
- })
+ :middleware wrap-dev})
diff --git a/env/dev/cljs/youyesyet/dev.cljs b/env/dev/cljs/youyesyet/dev.cljs
index 6394c48..a246523 100644
--- a/env/dev/cljs/youyesyet/dev.cljs
+++ b/env/dev/cljs/youyesyet/dev.cljs
@@ -1,5 +1,5 @@
-(ns ^:figwheel-no-load youyesyet.canvasser-app.app
- (:require [youyesyet.canvasser-app.core :as core]
+(ns ^:figwheel-no-load youyesyet.app
+ (:require [youyesyet.core :as core]
[devtools.core :as devtools]
[figwheel.client :as figwheel :include-macros true]))
diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn
index 0a5f540..6f14c22 100644
--- a/env/dev/resources/config.edn
+++ b/env/dev/resources/config.edn
@@ -1,7 +1,4 @@
{:dev true
:port 3000
;; when :nrepl-port is set the application starts the nREPL server on load
- :nrepl-port 7000
- :site-title "Project Hope"
- :site-logo "img/ProjectHopeLogo.png"
- :motd "motd.md"}
+ :nrepl-port 7000}
diff --git a/env/prod/clj/youyesyet/core.clj b/env/prod/clj/youyesyet/core.clj
deleted file mode 100644
index 3517ef9..0000000
--- a/env/prod/clj/youyesyet/core.clj
+++ /dev/null
@@ -1,67 +0,0 @@
-(ns ^{:doc "Production launcher, entirely boilerplate from Luminus."}
- youyesyet.core
- (:require [clojure.tools.cli :refer [parse-opts]]
- [clojure.tools.logging :as log]
- [luminus.http-server :as http]
- [luminus-migrations.core :as migrations]
- [mount.core :as mount]
- [youyesyet.config :refer [env]]
- [youyesyet.handler :as handler])
- (:gen-class))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;
-;;;; youyesyet.core: run as a standalone application. Entirely luminus
-;;;; boilerplate.
-;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(def cli-options
- [["-p" "--port PORT" "Port number"
- :parse-fn #(Integer/parseInt %)]])
-
-
-(mount/defstate ^{:on-reload :noop}
- http-server
- :start
- (http/start
- (-> env
- (assoc :handler handler/app)
- (update :port #(or (-> env :options :port) %))))
- :stop
- (http/stop http-server))
-
-
-(defn stop-app []
- (doseq [component (:stopped (mount/stop))]
- (log/info component "stopped"))
- (shutdown-agents))
-
-
-(defn init-jndi []
- (System/setProperty "java.naming.factory.initial"
- "org.apache.naming.java.javaURLContextFactory")
- (System/setProperty "java.naming.factory.url.pkgs"
- "org.apache.naming"))
-
-
-(defn start-app [args]
- (init-jndi)
- (doseq [component (-> args
- (parse-opts cli-options)
- mount/start-with-args
- :started)]
- (log/info component "started"))
- (.addShutdownHook (Runtime/getRuntime)
- (Thread. handler/destroy)))
-
-
-(defn -main [& args]
- (cond
- (some #{"migrate" "rollback"} args)
- (do
- (mount/start #'youyesyet.config/env)
- (migrations/migrate args (select-keys env [:database-url]))
- (System/exit 0))
- :else
- (start-app args)))
diff --git a/env/prod/cljs/youyesyet/prod.cljs b/env/prod/cljs/youyesyet/prod.cljs
index 8cc0292..ca12fd2 100644
--- a/env/prod/cljs/youyesyet/prod.cljs
+++ b/env/prod/cljs/youyesyet/prod.cljs
@@ -1,5 +1,5 @@
(ns youyesyet.app
- (:require [youyesyet.canvasser-app.core :as core]))
+ (:require [youyesyet.core :as core]))
;;ignore println statements in prod
(set! *print-fn* (fn [& _]))
diff --git a/env/prod/resources/config.edn b/env/prod/resources/config.edn
index 6c6da16..b48cfbd 100644
--- a/env/prod/resources/config.edn
+++ b/env/prod/resources/config.edn
@@ -1,6 +1,2 @@
{:production true
- :port 8765
- :site-title "Project Hope"
- :site-logo "img/ProjectHopeLogo.png"
- :motd "motd.md"
- :app-context "/youyesyet"}
+ :port 3000}
diff --git a/externs.js b/externs.js
index 55c082e..b190e01 100644
--- a/externs.js
+++ b/externs.js
@@ -1,14 +1,12 @@
-/* I'm not certain that this file is still needed at all; however, it doesn't seem to be doing any harm. */
/* Things which should not get renamed when compiling ClojureScript */
/* this block relates to the use of Leaflet */
var L = {
- "map": {
- "setView": function(){},
- "eg": function(){}
- },
- "tileLayer": {
+ "map": {
+ "setView": function(){}
+ },
+ "tileLayer": {
"addTo": function(){}
- }
+ }
};
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 14992a1..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,200 +0,0 @@
-{
- "requires": true,
- "lockfileVersion": 1,
- "dependencies": {
- "acorn": {
- "version": "4.0.13",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
- "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c="
- },
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
- },
- "core-util-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
- },
- "enhanced-resolve": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz",
- "integrity": "sha1-oRXDJQS2MC6Fp2Jp16V8zdli41k=",
- "requires": {
- "graceful-fs": "^4.1.2",
- "memory-fs": "^0.3.0",
- "object-assign": "^4.0.1",
- "tapable": "^0.2.3"
- }
- },
- "errno": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
- "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
- "requires": {
- "prr": "~1.0.1"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
- },
- "glob": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "graceful-fs": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
- "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
- },
- "isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
- },
- "memory-fs": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz",
- "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=",
- "requires": {
- "errno": "^0.1.3",
- "readable-stream": "^2.0.1"
- }
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "requires": {
- "wrappy": "1"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
- },
- "process-nextick-args": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
- "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
- },
- "prr": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
- "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
- },
- "readable-stream": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
- "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "resolve-from": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
- "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
- },
- "safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- },
- "tapable": {
- "version": "0.2.8",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
- "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI="
- },
- "tern": {
- "version": "0.21.0",
- "resolved": "https://registry.npmjs.org/tern/-/tern-0.21.0.tgz",
- "integrity": "sha1-gJyHqCbhEklDmM+IlPfC0bNGTrc=",
- "requires": {
- "acorn": "^4.0.9",
- "enhanced-resolve": "^2.2.2",
- "glob": "^7.1.1",
- "minimatch": "^3.0.3",
- "resolve-from": "2.0.0"
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
- }
- }
-}
diff --git a/project.clj b/project.clj
index b52f6a5..408db4a 100644
--- a/project.clj
+++ b/project.clj
@@ -1,194 +1,165 @@
-(defproject youyesyet "0.2.2"
+(defproject youyesyet "0.1.0-SNAPSHOT"
:description "Canvassing tool for referenda"
- :license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
- :url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
:url "https://github.com/simon-brooke/youyesyet"
- :dependencies [[adl-support "0.1.6"]
- [bouncer "1.0.1"]
- [ch.qos.logback/logback-classic "1.2.3"]
+ :dependencies [[org.clojure/clojure "1.8.0"]
+ [org.clojure/clojurescript "1.9.229" :scope "provided"]
+ [ring/ring-servlet "1.5.1"]
[clj-oauth "1.5.5"]
- [cljsjs/react-leaflet "1.6.5-0"]
- ;; [cljsjs/react-leaflet "2.0.1-0"] is available but doesn't seem to work fully
- [cljs-ajax "0.8.0"]
- [com.andrewmcveigh/cljs-time "0.5.2"]
- [clojure.java-time "0.3.2"]
- [com.cemerick/url "0.1.1"]
- [compojure "1.6.1"]
- [conman "0.8.3"]
- [cprop "0.1.13"]
- [day8.re-frame/http-fx "0.1.6"]
- [korma "0.4.3"]
- [lib-noir "0.9.9" :exclusions [org.clojure/tools.reader]]
- [luminus/ring-ttl-session "0.3.2"]
- [luminus-nrepl "0.1.6"]
- [luminus-migrations "0.6.5"]
- [luminus-immutant "0.2.5"]
- [markdown-clj "1.0.8"]
- [metosin/compojure-api "1.1.12"]
- [metosin/ring-http-response "0.9.1"]
- [migratus "1.2.3"]
- [mount "0.1.16"]
- [org.clojure/clojure "1.9.0"]
- [org.clojure/clojurescript "1.10.520" :scope "provided"]
- [org.clojure/core.memoize "0.7.1"]
- ;;[org.clojure/spec.alpha "0.2.168"]
- [org.clojure/tools.cli "0.4.2"]
- [org.clojure/tools.logging "0.4.1"]
- [org.postgresql/postgresql "42.2.5"]
- [org.webjars/bootstrap "4.3.1"]
- [org.webjars/font-awesome "5.8.1"]
- [org.webjars.bower/tether "1.4.4"]
- [postgre-types "0.0.4"]
- [re-frame "0.10.6"]
- [reagent "0.8.1"]
- [reagent-utils "0.3.2"]
- [ring-middleware-format "0.7.4"]
- [ring/ring-defaults "0.3.2"]
- [ring/ring-servlet "1.7.1"]
- [ring-webjars "0.2.0"]
+ [ch.qos.logback/logback-classic "1.2.2"]
+ [re-frame "0.9.2"]
+ [cljs-ajax "0.5.8"]
[secretary "1.2.3"]
- [selmer "1.12.12"]]
-
- :deploy-repositories [["releases" :clojars]
- ["snapshots" :clojars]]
+ [reagent-utils "0.2.1"]
+ [reagent "0.6.1"]
+ [korma "0.4.3"]
+ [selmer "1.10.6"]
+ [markdown-clj "0.9.98"]
+ [ring-middleware-format "0.7.2"]
+ [metosin/ring-http-response "0.8.2"]
+ [bouncer "1.0.1"]
+ [org.webjars/bootstrap "4.0.0-alpha.6-1"]
+ [org.webjars/font-awesome "4.7.0"]
+ [org.webjars.bower/tether "1.4.0"]
+ [org.clojure/tools.logging "0.3.1"]
+ [compojure "1.5.2"]
+ [metosin/compojure-api "1.1.10"]
+ [ring-webjars "0.1.1"]
+ [ring/ring-defaults "0.2.3"]
+ [luminus/ring-ttl-session "0.3.1"]
+ [mount "0.1.11"]
+ [cprop "0.1.10"]
+ [org.clojure/tools.cli "0.3.5"]
+ [migratus "0.8.33"]
+ [luminus-nrepl "0.1.4"]
+ [luminus-migrations "0.3.0"]
+ [conman "0.6.3"]
+ [org.postgresql/postgresql "9.4.1212"]
+ ]
:min-lein-version "2.0.0"
+ :license {:name "GNU General Public License v2"
+ :url "http://www.gnu.org/licenses/gpl-2.0.html"}
+
:jvm-opts ["-server" "-Dconf=.lein-env"]
:source-paths ["src/clj" "src/cljc"]
- :test-paths ["test/clj"]
:resource-paths ["resources" "target/cljsbuild"]
:target-path "target/%s/"
- :main ^:skip-aot youyesyet.core
+ :main youyesyet.core
:migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")}
- :plugins [[lein-adl "0.1.7"]
- [lein-cljsbuild "1.1.7"]
- [lein-codox "0.10.7-multilang"]
- [lein-cprop "1.0.3"]
- [lein-kibit "0.1.6"]
- [lein-less "1.7.5"]
- [lein-npm "0.6.2"]
- [lein-release "1.0.5"]
+ :plugins [[lein-cprop "1.0.1"]
+ [migratus-lein "0.4.2"]
+ [org.clojars.punkisdead/lein-cucumber "1.0.5"]
+ [lein-cljsbuild "1.1.4"]
[lein-uberwar "0.2.0"]
- [migratus-lein "0.5.9"]
- [org.clojars.punkisdead/lein-cucumber "1.0.7"]]
+ [lein-bower "0.5.1"]
+ [lein-less "1.7.5"]]
+
+ :bower-dependencies [
+ ;; Problem with using boostrap and font-awsome from Bower: neither
+ ;; of the distributed packages compile cleanly with less :-(
+ ;; [bootstrap "2.3.1"]
+ ;; [font-awesome "3.2.1"]
+ [leaflet "0.7.3"]]
:cucumber-feature-paths ["test/clj/features"]
- :codox {:metadata {:doc "**TODO**: write docs"
- :doc/format :markdown}
- :languages [:clojure :clojurescript]
- :source-paths ["src/clj" "src/cljc" "src/cljs"]
- :source-uri "https://github.com/simon-brooke/youyesyet/blob/master/{filepath}#L{line}"
- :output-path "docs"}
+ :hooks [leiningen.less]
+
+ :uberwar
+ {:handler youyesyet.handler/app
+ :init youyesyet.handler/init
+ :destroy youyesyet.handler/destroy
+ :name "youyesyet.war"}
+
+ :clean-targets ^{:protect false}
+ [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]]
+
+ :figwheel
+ {:http-server-root "public"
+ :nrepl-port 7002
+ :css-dirs ["resources/public/css"]
+ :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
+
+ :externs ["externs.js"]
+
+ :profiles
+ {:uberjar {:omit-source true
+ :prep-tasks ["compile" ["cljsbuild" "once" "min"]]
+ :cljsbuild
+ {:builds
+ {:min
+ {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"]
+ :compiler
+ {:output-to "target/cljsbuild/public/js/app.js"
+ :externs ["react/externs/react.js" "externs.js"]
+ :optimizations :advanced
+ :pretty-print false
+ :closure-warnings
+ {:externs-validation :off :non-standard-jsdoc :off}}}}}
- :npm {:dependencies [[datatables.net "1.10.19"]
- [datatables.net-dt "1.10.19"]
- [jquery "3.3.1"]
- [leaflet "1.3.1"]
- [selectize "0.12.6"]
- [signature_pad "2.3.2"]
- [simplemde "1.11.2"]]
- :root "resources/public/js/lib"}
+ :aot :all
+ :uberjar-name "youyesyet.jar"
+ :source-paths ["env/prod/clj"]
+ :resource-paths ["env/prod/resources"]}
- ;; `lein release` doesn't play nice with `git flow release`. Run `lein release` in the
- ;; `develop` branch, then reset the `master` branch to the release tag.
+ :dev [:project/dev :profiles/dev]
+ :test [:project/dev :project/test :profiles/test]
- :release-tasks [["vcs" "assert-committed"]
- ["change" "version" "leiningen.release/bump-version" "release"]
- ["vcs" "commit"]
- ["clean"]
- ["uberjar"]
- ["uberwar"]
- ["docker" "build"]
- ["docker" "push"]
- ["change" "version" "leiningen.release/bump-version"]
- ["vcs" "commit"]]
-
- :uberwar {:handler youyesyet.handler/app
- :init youyesyet.handler/init
- :destroy youyesyet.handler/destroy
- :name "youyesyet.war"}
-
- :clean-targets ^{:protect false} [:target-path [:cljsbuild :builds :app :compiler :output-dir]
- [:cljsbuild :builds :app :compiler :output-to]]
-
- :figwheel {:http-server-root "public"
- :nrepl-port 7002
- :css-dirs ["resources/public/css"]
- :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
+ :project/dev {:dependencies [[prone "1.1.4"]
+ [ring/ring-mock "0.3.0"]
+ [ring/ring-devel "1.5.1"]
+ [luminus-jetty "0.1.4"]
+ [pjstadig/humane-test-output "0.8.1"]
+ [org.clojure/core.cache "0.6.5"]
+ [org.apache.httpcomponents/httpcore "4.4.6"]
+ [clj-webdriver/clj-webdriver "0.7.2"]
+ [org.seleniumhq.selenium/selenium-server "3.3.1"]
+ [doo "0.1.7"]
+ [binaryage/devtools "0.9.2"]
+ [figwheel-sidecar "0.5.9"]
+ [com.cemerick/piggieback "0.2.2-SNAPSHOT"]
+ [directory-naming/naming-java "0.8"]]
+ :plugins [[com.jakemccrary/lein-test-refresh "0.14.0"]
+ [lein-doo "0.1.7"]
+ [lein-figwheel "0.5.8"]
+ [org.clojure/clojurescript "1.9.229"]]
+ :cljsbuild
+ {:builds
+ {:app
+ {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"]
+ :compiler
+ {:main "youyesyet.app"
+ :asset-path "/js/out"
+ :output-to "target/cljsbuild/public/js/app.js"
+ :output-dir "target/cljsbuild/public/js/out"
+ :source-map true
+ :optimizations :none
+ :pretty-print true}}}}
- :profiles {:uberjar {:omit-source true
- :prep-tasks ["adl"
- "compile"
- ["npm" "install"]
- ["cljsbuild" "once" "min"]]
- :cljsbuild
- {:builds
- {:min
- {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"]
- :compiler
- {:output-to "target/cljsbuild/public/js/app.js"
- :optimizations :advanced
- :pretty-print false
- :closure-warnings
- {:externs-validation :off :non-standard-jsdoc :off}
- :externs ["react/externs/react.js"]}}}}
- :aot :all
- :uberjar-name "youyesyet.jar"
- :source-paths ["env/prod/clj"]
- :resource-paths ["env/prod/resources"]}
- :dev [:project/dev :profiles/dev]
+ :doo {:build "test"}
+ :source-paths ["env/dev/clj" "test/clj"]
+ :resource-paths ["env/dev/resources"]
+ :repl-options {:init-ns user}
+ :injections [(require 'pjstadig.humane-test-output)
+ (pjstadig.humane-test-output/activate!)]}
+ :project/test {:resource-paths ["env/dev/resources" "env/test/resources"]
+ :cljsbuild
+ {:builds
+ {:test
+ {:source-paths ["src/cljc" "src/cljs" "test/cljs"]
+ :compiler
+ {:output-to "target/test.js"
+ :main "youyesyet.doo-runner"
+ :optimizations :whitespace
+ :pretty-print true}}}}
- :test [:project/dev :project/test :profiles/test]
-
- :project/dev {:dependencies [[prone "1.6.3"]
- [ring/ring-mock "0.4.0"]
- [ring/ring-devel "1.7.1"]
- [org.webjars/webjars-locator-jboss-vfs "0.1.0"]
- [luminus-immutant "0.2.5"]
- [pjstadig/humane-test-output "0.9.0"]
- [binaryage/devtools "0.9.10"]
- [com.cemerick/piggieback "0.2.2"]
- [directory-naming/naming-java "0.8"]
- [doo "0.1.11"]
- [figwheel-sidecar "0.5.18"]]
- :plugins [[com.jakemccrary/lein-test-refresh "0.23.0"]
- [lein-doo "0.1.10"]
- [lein-figwheel "0.5.16"]
- [org.clojure/clojurescript "1.10.520"]]
- :cljsbuild {:builds
- {:app
- {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"]
- :compiler
- {:main "youyesyet.canvasser-app.app"
- :asset-path "/js/out"
- :output-to "target/cljsbuild/public/js/app.js"
- :output-dir "target/cljsbuild/public/js/out"
- :source-map true
- :optimizations :none
- :pretty-print true}}}}
- :doo {:build "test"}
- :source-paths ["env/dev/clj"]
- :resource-paths ["env/dev/resources"]
- :repl-options {:init-ns user}
- :injections [(require 'pjstadig.humane-test-output)
- (pjstadig.humane-test-output/activate!)]}
- :project/test {:resource-paths ["env/test/resources"]
- :cljsbuild
- {:builds
- {:test
- {:source-paths ["src/cljc" "src/cljs" "test/cljs"]
- :compiler
- {:output-to "target/test.js"
- :main "youyesyet.doo-runner"
- :optimizations :whitespace
- :pretty-print true}}}}}
- :profiles/dev {}
- :profiles/test {}})
+ }
+ :profiles/dev {}
+ :profiles/test {}})
diff --git a/resources/about.md b/resources/about.md
deleted file mode 100644
index c2ba95a..0000000
--- a/resources/about.md
+++ /dev/null
@@ -1,69 +0,0 @@
-## Project Hope alpha test
-
-### What this is about
-
-This is a voter intention information system intended to be used by the 'Yes' side in the next Scottish independence referendum. Design documentation is [here](https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md).
-
-### The data in this demonstration system
-
-Although addresses in the database mostly are real, all personal data in the database is randomly generated and does not represent real people.
-
-### Logging into the demonstration system
-
-At present, you may log in in one of six roles, with the following usernames:
-
-* `test_admin` - someone who administers the whole system;
-* `test_analyst` - someone who has a general view of all the data in the system;
-* `test_canvasser` - someone who goes out and knocks on electors' doors to ask their voting intention;
-* `test_editor` - someone who can add and edit the issues canvassers will discuss with electors;
-* `test_expert` - an expert on one or more issues, who can respond by telephone or email to followup requests from electors;
-* `test_organiser` - someone who organises a team of canvassers.
-
-In each case, the password is exactly the same as the username.
-
-As each different class of user, you'll see a different view of the system, with only the features you're entitled to use available.
-
-### The app
-
-If you log in as `test_canvasser`, one of the options you will see is the **App**. The app is intended to be used by canvassers as they go door to door talking to electors. Consequently, it's designed to work on mobile phones.
-
-### The current state of the software
-
-Most of the planned funtionality is in place, but in this preliminary demo you cannot create, edit or delete any records.
-
-### How the project is managed
-
-The project is managed through [Github](https://github.com/simon-brooke/youyesyet). Please add issues that you encounter there. If you intend to collaborate on the project, I strongly recommend you also sign up to [ZenHub](https://www.zenhub.com/) (it's free; you'll need a Github login first).
-
-### Help wanted
-
-#### UX and CSS
-
-The existing stylesheets are ones I hacked up myself. I'm a software geek, not a visual designer. I'm sure something better can be done. Documentation on the user interface design is [here](https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/userspec.md). Volunteers?
-
-#### Database administration
-
-I have done preliminary work on how to scale the database, see [here](https://github.com/simon-brooke/youyesyet/blob/master/doc/specification/scaling.md). I think this will roughly work. However, I'd appreciate someone with more experience of scaling large web systems taking a look at my plan, and, ideally, taking over responsibility for this side of the project. Especially, having someone on the team with [Kubernetes](https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/) experience would be really helpful.
-
-#### Testing
-
-At this stage you can't really test this, since you can't modify data. But you can see whether the functionality you would need to perform your role is present, and you can suggest modifications. To do so, please raise issues [here](https://github.com/simon-brooke/youyesyet/issues).
-
-**In particular** it would be helpful, if the app **does not** work on your mobile phone, if you could add details of your phone (and what didn't work) [here](https://github.com/simon-brooke/youyesyet/issues/56).
-
-#### Coding
-
-The bulk of this system is written in [Clojure](https://clojure.org/). However, additional user interface functionality could be added in Javascript technologies, particularly React; and data analysis tools could be written in any language, although I'd particularly recommend work on using the data with [QGIS](https://qgis.org/en/site/).
-
-In any case, whichever technologies you are skilled in, help would be very welcome. If you'd like to become involved, please let [me](mailto:simon@journeyman.cc) know; but [pull requests](https://help.github.com/articles/about-pull-requests/) which address [current issues](https://github.com/simon-brooke/youyesyet/issues) will be welcomed.
-
-#### Training and training materials
-
-If this system is going to be of use to the **Yes** movement, we're going to have to train thousands of people to use it, and we're going to have to train them fast. I've tried to design the system to be as simple as possible, but training materials will be necessary. Help with these would be extremely welcome.
-
-#### Direction and Governance
-
-I've boostrapped this project pretty much on my own. If it's going to be of use to the **Yes** movement, it's going to need to be directed from here on in by the **Yes** movement. I've built it very much in the hope that it might be adopted by the [Radical Independence Campaign](http://radical.scot/), but whether RIC does choose to adopt it or not, there will need to be a group of people both to direct the project and to help find resources for it. Is this something you could do?
-
-
-Simon Brooke, 19th July 2018
diff --git a/resources/docs/docs.md b/resources/docs/docs.md
new file mode 100644
index 0000000..5930be1
--- /dev/null
+++ b/resources/docs/docs.md
@@ -0,0 +1,35 @@
+
+
+### Database Configuration is Required
+
+If you haven't already, then please follow the steps below to configure your database connection and run the necessary migrations.
+
+* Create the database for your application.
+* Update the connection URL in the `profiles.clj` file with your database name and login.
+* Run `lein run migrate` in the root of the project to create the tables.
+* Let `mount` know to start the database connection by `require`-ing youyesyet.db.core in some other namespace.
+* Restart the application.
+
+
+
+
+### Managing Your Middleware
+
+Request middleware functions are located under the `youyesyet.middleware` namespace.
+
+This namespace is reserved for any custom middleware for the application. Some default middleware is
+already defined here. The middleware is assembled in the `wrap-base` function.
+
+Middleware used for development is placed in the `youyesyet.dev-middleware` namespace found in
+the `env/dev/clj/` source path.
+
+### Here are some links to get started
+
+1. [HTML templating](http://www.luminusweb.net/docs/html_templating.md)
+2. [Accessing the database](http://www.luminusweb.net/docs/database.md)
+3. [Setting response types](http://www.luminusweb.net/docs/responses.md)
+4. [Defining routes](http://www.luminusweb.net/docs/routes.md)
+5. [Adding middleware](http://www.luminusweb.net/docs/middleware.md)
+6. [Sessions and cookies](http://www.luminusweb.net/docs/sessions_cookies.md)
+7. [Security](http://www.luminusweb.net/docs/security.md)
+8. [Deploying the application](http://www.luminusweb.net/docs/deployment.md)
diff --git a/resources/sql/locality-trigger.sql b/resources/migrations/20161014170335-basic-setup.down.sql
similarity index 60%
rename from resources/sql/locality-trigger.sql
rename to resources/migrations/20161014170335-basic-setup.down.sql
index 00452b3..1005cee 100644
--- a/resources/sql/locality-trigger.sql
+++ b/resources/migrations/20161014170335-basic-setup.down.sql
@@ -1,6 +1,6 @@
-------------------------------------------------------------------------------;
+--------------------------------------------------------------------------------
----
----- locality-trigger.sql: compute localities for addresses
+---- 20161014170335-basic-setup.down.sql: database schema for youyesyet.
----
---- This program is free software; you can redistribute it and/or
---- modify it under the terms of the GNU General Public License
@@ -19,17 +19,32 @@
----
---- Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
----
-------------------------------------------------------------------------------;
+--------------------------------------------------------------------------------
----- See also: src/cljc/locality.cljc
+-- intended to reverse out the database changes made in
+-- 20161014170335-basic-setup.up.sql
-CREATE FUNCTION compute_locality() RETURNS trigger AS $compute_locality$
- BEGIN
- NEW.locality = (1000 * floor(NEW.latitude * 100)) -
- floor(NEW.longitude * 100);
- RETURN NEW;
- END;
-$compute_locality$ LANGUAGE plpgsql;
-
-CREATE TRIGGER compute_locality BEFORE INSERT OR UPDATE ON addresses
- FOR EACH ROW EXECUTE PROCEDURE compute_locality();
+drop table addresses cascade;
+--;;
+drop table authorities cascade;
+--;;
+drop table canvassers cascade;
+--;;
+drop table districts cascade;
+--;;
+drop table electors cascade;
+--;;
+drop table followupactions cascade;
+--;;
+drop table followupmethods cascade;
+--;;
+drop table followuprequests cascade;
+--;;
+drop table issueexpertise cascade;
+--;;
+drop table issues cascade;
+--;;
+drop table options cascade;
+--;;
+drop table visits cascade;
+--;;
diff --git a/resources/migrations/20161014170335-basic-setup.up.sql b/resources/migrations/20161014170335-basic-setup.up.sql
new file mode 100644
index 0000000..0719e57
--- /dev/null
+++ b/resources/migrations/20161014170335-basic-setup.up.sql
@@ -0,0 +1,666 @@
+--------------------------------------------------------------------------------
+----
+---- 20161014170335-basic-setup.up.sql: database schema for youyesyet.
+----
+---- This program is free software; you can redistribute it and/or
+---- modify it under the terms of the GNU General Public License
+---- as published by the Free Software Foundation; either version 2
+---- of the License, or (at your option) any later version.
+----
+---- This program is distributed in the hope that it will be useful,
+---- but WITHOUT ANY WARRANTY; without even the implied warranty of
+---- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+---- GNU General Public License for more details.
+----
+---- You should have received a copy of the GNU General Public License
+---- along with this program; if not, write to the Free Software
+---- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+---- USA.
+----
+---- Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
+----
+--------------------------------------------------------------------------------
+----
+---- NOTE
+---- This file is essentially a Postgres schema dump of a database schema which was
+---- created with the function initdb! in the file src/clj/youyesyet/db/schema.clj.
+---- This file has then been mildly massaged to work with Migratus.
+---- Either this file or src/clj/youyesyet/db/schema.clj is redundant; schema.clj
+---- represents the older, Korma, way of doing things but does not readily allow
+---- for migrations; this file represents the newer Migratus/HugSQL way. I'm not
+---- certain which of these paths I'm going to go down.
+----
+--------------------------------------------------------------------------------
+
+SET statement_timeout = 0;
+--;;
+SET lock_timeout = 0;
+--;;
+SET client_encoding = 'UTF8';
+--;;
+SET standard_conforming_strings = on;
+--;;
+SET check_function_bodies = false;
+--;;
+SET client_min_messages = warning;
+--;;
+--
+-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
+--
+
+CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
+--;;
+
+--
+-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
+--
+
+COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
+--;;
+
+SET search_path = public, pg_catalog;
+--;;
+SET default_tablespace = '';
+--;;
+SET default_with_oids = false;
+--;;
+--
+-- Name: addresses; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS addresses (
+ id integer NOT NULL,
+ address character varying(256) NOT NULL,
+ postcode character varying(16),
+ phone character varying(16),
+ district_id integer,
+ latitude real,
+ longitude real
+);
+--;;
+
+ALTER TABLE public.addresses OWNER TO youyesyet;
+--;;
+--
+-- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
+--
+
+CREATE SEQUENCE addresses_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+--;;
+
+ALTER TABLE public.addresses_id_seq OWNER TO youyesyet;
+--;;
+--
+-- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
+--
+
+ALTER SEQUENCE addresses_id_seq OWNED BY addresses.id;
+--;;
+
+--
+-- Name: authorities; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS authorities (
+ id character varying(32) NOT NULL
+);
+--;;
+
+ALTER TABLE public.authorities OWNER TO youyesyet;
+--;;
+--
+-- Name: canvassers; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS canvassers (
+ id serial,
+ username character varying(32) NOT NULL,
+ fullname character varying(64) NOT NULL,
+ elector_id integer,
+ address_id integer NOT NULL,
+ phone character varying(16),
+ email character varying(128),
+ authority_id character varying(32) NOT NULL,
+ authorised boolean
+);
+--;;
+
+ALTER TABLE public.canvassers OWNER TO youyesyet;
+--;;
+--
+-- Name: districts; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS districts (
+ id integer NOT NULL,
+ name character varying(64) NOT NULL
+);
+--;;
+
+ALTER TABLE public.districts OWNER TO youyesyet;
+--;;
+--
+-- Name: electors; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS electors (
+ id integer NOT NULL,
+ name character varying(64) NOT NULL,
+ address_id integer NOT NULL,
+ phone character varying(16),
+ email character varying(128)
+);
+--;;
+
+ALTER TABLE public.electors OWNER TO youyesyet;
+--;;
+--
+-- Name: followupactions; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS followupactions (
+ id integer NOT NULL,
+ request_id integer NOT NULL,
+ actor integer NOT NULL,
+ date timestamp with time zone DEFAULT now() NOT NULL,
+ notes text,
+ closed boolean
+);
+--;;
+
+ALTER TABLE public.followupactions OWNER TO youyesyet;
+
+--
+-- Name: followupactions_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
+--
+
+CREATE SEQUENCE followupactions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+--;;
+
+ALTER TABLE public.followupactions_id_seq OWNER TO youyesyet;
+--;;
+--
+-- Name: followupactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
+--
+
+ALTER SEQUENCE followupactions_id_seq OWNED BY followupactions.id;
+--;;
+
+--
+-- Name: followupmethods; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS followupmethods (
+ id character varying(32) NOT NULL
+);
+--;;
+
+ALTER TABLE public.followupmethods OWNER TO youyesyet;
+--;;
+--
+-- Name: followuprequests; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+insert into followupmethods values ('Telephone');
+--;;
+insert into followupmethods values ('eMail');
+--;;
+insert into followupmethods values ('Post');
+--;;
+
+
+CREATE TABLE IF NOT EXISTS followuprequests (
+ id integer NOT NULL,
+ elector_id integer NOT NULL,
+ visit_id integer NOT NULL,
+ issue_id character varying(32) NOT NULL,
+ method_id character varying(32) NOT NULL
+);
+--;;
+
+ALTER TABLE public.followuprequests OWNER TO youyesyet;
+--;;
+
+--
+-- Name: followuprequests_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
+--
+
+CREATE SEQUENCE followuprequests_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+--;;
+
+
+ALTER TABLE public.followuprequests_id_seq OWNER TO youyesyet;
+--;;
+
+--
+-- Name: followuprequests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
+--
+
+ALTER SEQUENCE followuprequests_id_seq OWNED BY followuprequests.id;
+--;;
+
+
+--
+-- Name: issueexpertise; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS issueexpertise (
+ canvasser_id integer NOT NULL,
+ issue_id character varying(32) NOT NULL,
+ method_id character varying(32) NOT NULL
+);
+--;;
+
+
+ALTER TABLE public.issueexpertise OWNER TO youyesyet;
+--;;
+
+--
+-- Name: issues; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS issues (
+ id character varying(32) NOT NULL,
+ url character varying(256)
+);
+--;;
+
+
+ALTER TABLE public.issues OWNER TO youyesyet;
+--;;
+
+--
+-- Name: options; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS options (
+ id character varying(32) NOT NULL
+);
+--;;
+
+
+ALTER TABLE public.options OWNER TO youyesyet;
+--;;
+
+--
+-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS schema_migrations (
+ id bigint NOT NULL
+);
+--;;
+
+
+ALTER TABLE public.schema_migrations OWNER TO youyesyet;
+--;;
+
+--
+-- Name: visits; Type: TABLE; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+CREATE TABLE IF NOT EXISTS visits (
+ id integer NOT NULL,
+ address_id integer NOT NULL,
+ canvasser_id integer NOT NULL,
+ date timestamp with time zone DEFAULT now() NOT NULL
+);
+--;;
+
+
+ALTER TABLE public.visits OWNER TO youyesyet;
+--;;
+
+--
+-- Name: visits_id_seq; Type: SEQUENCE; Schema: public; Owner: youyesyet
+--
+
+CREATE SEQUENCE visits_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+--;;
+
+
+ALTER TABLE public.visits_id_seq OWNER TO youyesyet;
+--;;
+
+--
+-- Name: visits_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: youyesyet
+--
+
+ALTER SEQUENCE visits_id_seq OWNED BY visits.id;
+--;;
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY addresses ALTER COLUMN id SET DEFAULT nextval('addresses_id_seq'::regclass);
+--;;
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followupactions ALTER COLUMN id SET DEFAULT nextval('followupactions_id_seq'::regclass);
+--;;
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followuprequests ALTER COLUMN id SET DEFAULT nextval('followuprequests_id_seq'::regclass);
+--;;
+
+
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY visits ALTER COLUMN id SET DEFAULT nextval('visits_id_seq'::regclass);
+--;;
+
+
+--
+-- Name: addresses_address_key; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY addresses
+ ADD CONSTRAINT addresses_address_key UNIQUE (address);
+--;;
+
+
+--
+-- Name: addresses_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY addresses
+ ADD CONSTRAINT addresses_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: authorities_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY authorities
+ ADD CONSTRAINT authorities_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: canvassers_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY canvassers
+ ADD CONSTRAINT canvassers_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: districts_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY districts
+ ADD CONSTRAINT districts_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: electors_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY electors
+ ADD CONSTRAINT electors_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: followupactions_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY followupactions
+ ADD CONSTRAINT followupactions_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: followupmethods_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY followupmethods
+ ADD CONSTRAINT followupmethods_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: followuprequests_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY followuprequests
+ ADD CONSTRAINT followuprequests_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: issues_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY issues
+ ADD CONSTRAINT issues_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: options_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY options
+ ADD CONSTRAINT options_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: schema_migrations_id_key; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+--ALTER TABLE ONLY schema_migrations
+-- ADD CONSTRAINT schema_migrations_id_key UNIQUE (id);
+--;;
+
+
+--
+-- Name: visits_pkey; Type: CONSTRAINT; Schema: public; Owner: youyesyet; Tablespace:
+--
+
+ALTER TABLE ONLY visits
+ ADD CONSTRAINT visits_pkey PRIMARY KEY (id);
+--;;
+
+
+--
+-- Name: addresses_district_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY addresses
+ ADD CONSTRAINT addresses_district_id_fkey FOREIGN KEY (district_id) REFERENCES districts(id);
+--;;
+
+
+--
+-- Name: canvassers_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY canvassers
+ ADD CONSTRAINT canvassers_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
+--;;
+
+
+--
+-- Name: canvassers_authority_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY canvassers
+ ADD CONSTRAINT canvassers_authority_id_fkey FOREIGN KEY (authority_id) REFERENCES authorities(id);
+--;;
+
+
+--
+-- Name: canvassers_elector_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY canvassers
+ ADD CONSTRAINT canvassers_elector_id_fkey FOREIGN KEY (elector_id) REFERENCES electors(id);
+--;;
+
+
+--
+-- Name: electors_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY electors
+ ADD CONSTRAINT electors_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
+--;;
+
+
+--
+-- Name: followupactions_actor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followupactions
+ ADD CONSTRAINT followupactions_actor_fkey FOREIGN KEY (actor) REFERENCES canvassers(id);
+--;;
+
+
+--
+-- Name: followupactions_request_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followupactions
+ ADD CONSTRAINT followupactions_request_id_fkey FOREIGN KEY (request_id) REFERENCES followuprequests(id);
+--;;
+
+
+--
+-- Name: followuprequests_elector_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followuprequests
+ ADD CONSTRAINT followuprequests_elector_id_fkey FOREIGN KEY (elector_id) REFERENCES electors(id);
+--;;
+
+
+--
+-- Name: followuprequests_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followuprequests
+ ADD CONSTRAINT followuprequests_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id);
+--;;
+
+
+--
+-- Name: followuprequests_method_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followuprequests
+ ADD CONSTRAINT followuprequests_method_id_fkey FOREIGN KEY (method_id) REFERENCES followupmethods(id);
+--;;
+
+
+--
+-- Name: followuprequests_visit_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY followuprequests
+ ADD CONSTRAINT followuprequests_visit_id_fkey FOREIGN KEY (visit_id) REFERENCES visits(id);
+--;;
+
+
+--
+-- Name: issueexpertise_canvasser_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY issueexpertise
+ ADD CONSTRAINT issueexpertise_canvasser_id_fkey FOREIGN KEY (canvasser_id) REFERENCES canvassers(id);
+--;;
+
+
+--
+-- Name: issueexpertise_issue_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY issueexpertise
+ ADD CONSTRAINT issueexpertise_issue_id_fkey FOREIGN KEY (issue_id) REFERENCES issues(id);
+--;;
+
+
+--
+-- Name: issueexpertise_method_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY issueexpertise
+ ADD CONSTRAINT issueexpertise_method_id_fkey FOREIGN KEY (method_id) REFERENCES followupmethods(id);
+--;;
+
+
+--
+-- Name: visits_address_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY visits
+ ADD CONSTRAINT visits_address_id_fkey FOREIGN KEY (address_id) REFERENCES addresses(id);
+--;;
+
+
+--
+-- Name: visits_canvasser_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: youyesyet
+--
+
+ALTER TABLE ONLY visits
+ ADD CONSTRAINT visits_canvasser_id_fkey FOREIGN KEY (canvasser_id) REFERENCES canvassers(id);
+--;;
+
+
+--
+-- Name: public; Type: ACL; Schema: -; Owner: postgres
+--
+
+REVOKE ALL ON SCHEMA public FROM PUBLIC;
+--;;
+
+REVOKE ALL ON SCHEMA public FROM postgres;
+--;;
+
+GRANT ALL ON SCHEMA public TO postgres;
+--;;
+
+GRANT ALL ON SCHEMA public TO PUBLIC;
+--;;
+
+
+--
+-- PostgreSQL database dump complete
+--
+
diff --git a/resources/migrations/20170315190500-roles-and-teams.down.sql b/resources/migrations/20170315190500-roles-and-teams.down.sql
new file mode 100644
index 0000000..a8bdb6a
--- /dev/null
+++ b/resources/migrations/20170315190500-roles-and-teams.down.sql
@@ -0,0 +1,13 @@
+drop table teammemberships;
+
+drop table teamorganiserships;
+
+drop index ix_teams_name;
+
+drop table teams;
+
+drop table rolememberships;
+
+drop index ix_roles_name;
+
+drop table roles;
diff --git a/resources/migrations/20170315190500-roles-and-teams.up.sql b/resources/migrations/20170315190500-roles-and-teams.up.sql
new file mode 100644
index 0000000..7ff7ffc
--- /dev/null
+++ b/resources/migrations/20170315190500-roles-and-teams.up.sql
@@ -0,0 +1,39 @@
+create table if not exists roles (
+ id serial primary key,
+ name varchar(64) not null
+);
+
+create unique index ix_roles_name on roles(name);
+
+create table if not exists rolememberships (
+ role_id integer not null references roles(id),
+ canvasser_id integer not null references canvassers(id)
+);
+
+create table if not exists teams (
+ id serial primary key,
+ name varchar(64) not null,
+ district_id integer not null references districts(id),
+ latitude real,
+ longitude real
+);
+
+create unique index ix_teams_name on teams(name);
+
+create table if not exists teammemberships (
+ team_id integer not null references teams(id),
+ canvasser_id integer not null references canvassers(id)
+);
+
+create table if not exists teamorganiserships (
+ team_id integer not null references teams(id),
+ canvasser_id integer not null references canvassers(id)
+);
+
+alter table roles owner to youyesyet;
+
+alter table rolememberships owner to youyesyet;
+
+alter table teams owner to youyesyet;
+
+alter table teammemberships owner to youyesyet;
diff --git a/resources/motd.md b/resources/motd.md
deleted file mode 100644
index f6de732..0000000
--- a/resources/motd.md
+++ /dev/null
@@ -1,9 +0,0 @@
-## Welcome to Project Hope
-
-### Alpha test code
-
-This is a voter intention information system intended to be used by the 'Yes' side in the next Scottish independence referendum. For more information, see [About](about).
-
-### App not working in Firefox
-
-This is a known issue; I apologise. It will be fixed.
diff --git a/resources/public/about.html b/resources/public/about.html
new file mode 100644
index 0000000..370071f
--- /dev/null
+++ b/resources/public/about.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ About YouYesYet
+
+
+
+
+ This isn't finished and doesn't work yet! This site is just a look-and-feel
+ dummy.
+
+
+ YouYesYet is a project to build a canvassing app for the new Scottish
+ Independence Referendum. The source code is here. The specification
+ is here.
+
+
+ If we're going to get this working in time I cannot do it alone: I need help. Contact
+ me by email or on on Twitter.
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer tincidunt
+ at ex id pretium. Proin nec ultricies mauris. Donec mattis, velit at commodo
+ vehicula, nisi velit mattis justo, at tempor enim eros eget tortor. Quisque
+ a porttitor lorem. Vestibulum tempus ex id sem laoreet, id fermentum enim
+ pharetra. Cras diam ante, pulvinar sed pharetra sed, venenatis eget tellus.
+ Quisque fermentum sem sed nulla mollis, et fermentum nisl pretium. Pellentesque
+ porttitor interdum ultricies. Nunc ut accumsan leo, rutrum tempor tellus.
+ Nam ultricies magna ipsum.
+
+
+ Pellentesque in est rutrum, consectetur nisi vel, dictum felis. Quisque id
+ elementum enim. Donec aliquet, massa id mattis semper, lectus elit scelerisque
+ justo, quis dapibus tortor eros a erat. Vestibulum erat mauris, consectetur id
+ condimentum ut, luctus vitae diam. Integer faucibus ultrices mi sed consequat.
+ Aliquam lacinia sapien quis urna blandit, sed consectetur ligula gravida. Ut
+ eleifend purus id mi vulputate faucibus ut quis risus. Donec dapibus finibus
+ tincidunt. Nunc luctus libero tellus, eget porta diam lacinia vel. Pellentesque
+ turpis nunc, venenatis vitae nisl eu, mollis pulvinar erat. Nulla scelerisque
+ tellus eget ex hendrerit tincidunt.
+
+
+ Duis tincidunt iaculis magna, ac rutrum velit congue quis. Maecenas feugiat
+ efficitur sem, in hendrerit erat. Nunc congue, dui sit amet commodo faucibus,
+ enim nisl feugiat nisl, a tincidunt massa metus nec nisi. Duis viverra nunc ut
+ libero tempus, sed convallis elit dapibus. Sed venenatis condimentum odio, non
+ elementum diam. Morbi fermentum metus justo, ac viverra dui fermentum at.
+ Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
+ cubilia Curae; Aliquam erat volutpat.
+
- We are suffering an intermittent problem causing an occasional crash of
- the Project Hope canvassing application. We apologise for this, and are
- working on a permanent fix.
-
-
- The application will restart automatically
- within five minutes, please take a short break.
-
+
+
+
diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql
index e5404bf..4191f67 100644
--- a/resources/sql/queries.sql
+++ b/resources/sql/queries.sql
@@ -1,87 +1,21 @@
-------------------------------------------------------------------------------;
-----
----- queries.sql: manually maintained queries.
-----
----- This program is free software; you can redistribute it and/or
----- modify it under the terms of the GNU General Public License
----- 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 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)
--- This file gets slurped in and converted into simple functions by the line
--- in youyesyet.db.core.clj:
--- (conman/bind-connection *db* "sql/queries-auto.sql" "sql/queries.sql")
--- the functions then appeare in the youyesyet.db.core namespace.
--- Note that queries generated by ADL are in the file
--- resources/sql/queries-auto.sql; they do not have to be (and should not be)
--- redefined here.
+-- :name update-user! :! :n
+-- :doc update an existing user record
+UPDATE users
+SET first_name = :first_name, last_name = :last_name, email = :email
+WHERE id = :id
--- :name list-addresses-by-locality :? :*
--- :doc lists all existing address records in a given locality
-SELECT *
-FROM addresses
-WHERE locality = :locality
+-- :name get-user :? :1
+-- :doc retrieve a user given the id.
+SELECT * FROM users
+WHERE id = :id
-
--- :name list-open-requests :? :*
--- :doc lists all existing followuprequest records which have not been closed and which the :expert has expertise to answer.
-SELECT DISTINCT request.*,
- electors.name ||', '|| electors.gender AS elector_id_expanded,
- addresses.address ||', '|| addresses.postcode ||', '|| visits.date AS visit_id_expanded,
- request.issue_id as issue_id_expanded,
- request.method_id AS method_id_expanded,
- visits.date AS raised
-FROM followuprequests as request,
- ln_experts_issues_canvassers as expertise,
- canvassers as experts,
- electors,
- addresses,
- visits
-where not exists (select * from followupactions as action
- where action.request_id = request.id
- and action.closed = true)
-and request.locked_by is null
-and request.elector_id = electors.id
-and request.visit_id = visits.id
-and visits.address_id = addresses.id
-and request.issue_id = expertise.issue_id
-and expertise.canvasser_id = :expert
-ORDER BY raised
-
---:name get-last-visit-by-canvasser :? :1
---:doc returns the most recent visit record of the canvasser with the specified `:id`
-SELECT * FROM visits
-WHERE canvasser_id = :id
-ORDER BY date desc
-LIMIT 1
-
---:name get-locality-for-visit :? :1
---:doc returns the locality of the address of this visit
-SELECT addresses.locality
-FROM addresses, visits
-WHERE visits.address_id = addresses.id
-AND visits.id = :id
-
-
--- :name list-elector-intentions :? :*
--- :doc short form of `list-intentions-by-elector`, returning far less data, for use in `youyesyet.routes.rest/get-local-data`, q.v.
--- TODO: should be limited to visits in the past 24 hours, to prevent the app being
--- used to harrass NO voters. See https://github.com/simon-brooke/youyesyet/issues/58
-SELECT intentions.id, intentions.option_id, visits.date
-FROM intentions, visits
-WHERE intentions.visit_id = visits.id
-AND intentions.elector_id = :id
-ORDER BY visits.date DESC
+-- :name delete-user! :! :n
+-- :doc delete a user given the id
+DELETE FROM users
+WHERE id = :id
diff --git a/resources/sql/test-canvassers.sql b/resources/sql/test-canvassers.sql
deleted file mode 100644
index 0bfb6b8..0000000
--- a/resources/sql/test-canvassers.sql
+++ /dev/null
@@ -1,65 +0,0 @@
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_admin', 'Michael Thomson', 14, 4, 'Twitter');
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_analyst', 'Jack Lang', 13, 4, 'Twitter');
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_canvasser', 'Catriona Lang', 12, 4, 'Twitter');
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_editor', 'Ursula Lang', 11, 4, 'Twitter');
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_expert', 'Charlie Gourlay', 18, 5, 'Twitter');
-
-insert into canvassers (username, fullname, elector_id, address_id, authority_id)
- values ('test_organiser', 'Jude Morrison', 15, 5, 'Twitter');
-
-insert into ln_members_roles_canvassers
- values (1, (select id from canvassers where username='test_admin'));
-insert into ln_members_roles_canvassers
- values (2, (select id from canvassers where username='test_analyst'));
-insert into ln_members_roles_canvassers
- values (3, (select id from canvassers where username='test_editor'));
-insert into ln_members_roles_canvassers
- values (4, (select id from canvassers where username='test_organiser'));
-insert into ln_members_roles_canvassers
- values (5, (select id from canvassers where username='test_expert'));
-insert into ln_members_roles_canvassers
- values (6, (select id from canvassers where username='test_canvasser'));
-
-insert into teams (name, district_id, latitude, longitude)
-values ('Yes Stewartry', 1, 54.94, -3.94);
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_admin'));
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_analyst'));
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_editor'));
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_organiser'));
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_expert'));
-
-insert into ln_members_teams_canvassers
-values (
- (select id from teams where name='Yes Stewartry'),
- (select id from canvassers where username='test_canvasser'));
-
diff --git a/resources/sql/youyesyet.postgres.overrides.sql b/resources/sql/youyesyet.postgres.overrides.sql
deleted file mode 100644
index f716dbf..0000000
--- a/resources/sql/youyesyet.postgres.overrides.sql
+++ /dev/null
@@ -1,53 +0,0 @@
-------------------------------------------------------------------------
--- User `youyesyet` (the app, and less secure parts of the site)
--- must have the permissions of `canvassers`.
-------------------------------------------------------------------------
-DO
-$do$
-BEGIN
- IF NOT EXISTS (
- SELECT -- SELECT list can stay empty for this
- FROM pg_catalog.pg_roles
- WHERE rolname = 'youyesyet') THEN
-
- CREATE ROLE youyesyet LOGIN PASSWORD 'thisisnotsecure';
- END IF;
-END
-$do$;
-
-grant canvassers to youyesyet;
-
-------------------------------------------------------------------------
--- convenience view lv_followupactions of entity followupactions for
--- lists, et cetera
--- ADL is not yet correctly chaining tables when generating convenience
--- views, so the auto-generated convenience view is a horrible
--- cross-product join
-------------------------------------------------------------------------
-DROP VIEW lv_followupactions;
-CREATE VIEW lv_followupactions AS
-SELECT electors.name ||', '|| addresses.address ||', '|| addresses.postcode ||', '|| visits.date ||', '|| issues.id AS request_id_expanded,
- followupactions.request_id,
- canvassers.username ||', '|| canvassers.fullname ||', '|| addresses.address ||', '|| addresses.postcode ||', '|| canvassers.phone ||', '|| canvassers.email AS actor_expanded,
- followupactions.actor,
- canvassers.fullname AS actor_name,
- followupactions.date,
- followupactions.notes,
- followupactions.closed,
- followupactions.id
-FROM followuprequests, visits, canvassers, addresses, followupactions, issues, electors
-WHERE followupactions.request_id = followuprequests.id
- AND followuprequests.elector_id = electors.id
- AND followuprequests.visit_id = visits.id
- AND followuprequests.issue_id = issues.id
- AND visits.address_id = addresses.id
- AND followupactions.actor = canvassers.id
-;
-GRANT SELECT ON lv_followupactions TO canvassers, issueexperts;
-
-
-------------------------------------------------------------------------
--- request locking
-------------------------------------------------------------------------
-ALTER TABLE followuprequests ADD COLUMN locked_by INTEGER REFERENCES canvassers(id) ON DELETE SET NULL;
-ALTER TABLE followuprequests ADD COLUMN locked TIMESTAMP;
diff --git a/resources/templates/app.html b/resources/templates/app.html
deleted file mode 100644
index acf8274..0000000
--- a/resources/templates/app.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "base.html" %}
-{% block head %}
-
-
-
-
-
-
- {{site-title}}: {{title}}
-{% endblock %}
-{% block whole-page %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- You Yes Yet is loading.
- You must enable JavaScript to use the You Yes Yet app.
-
- This website stores session information as a 'cookie' on your browser.
- This helps us show you the content you want to see. This cookie does
- not identify you, and cannot be read by other websites. It is deleted
- by your browser as soon as you leave this site. This website does not
- use any third party cookies, so your visit here cannot be tracked by
- other websites.
-