Starting to get the project set up. Nothing is even nearly complete yet.

This commit is contained in:
simon 2016-10-13 14:25:54 +01:00
commit b6a24bc1ce
59 changed files with 7118 additions and 0 deletions

28
Capstanfile Normal file
View file

@ -0,0 +1,28 @@
#
# Name of the base image. Capstan will download this automatically from
# Cloudius S3 repository.
#
#base: cloudius/osv
base: cloudius/osv-openjdk8
#
# The command line passed to OSv to start up the application.
#
cmdline: /java.so -jar /youyesyet/app.jar
#
# The command to use to build the application.
# You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine
#
# For Leiningen, you can use:
#build: lein uberjar
# For Boot, you can use:
#build: boot build
#
# List of files that are included in the generated image.
#
files:
/youyesyet/app.jar: ./target/uberjar/youyesyet.jar

8
Dockerfile Normal file
View file

@ -0,0 +1,8 @@
FROM java:8-alpine
MAINTAINER Your Name <you@example.com>
ADD target/uberjar/youyesyet.jar /youyesyet/app.jar
EXPOSE 3000
CMD ["java", "-jar", "/youyesyet/app.jar"]

258
LICENSE Normal file
View file

@ -0,0 +1,258 @@
# GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to guarantee
your freedom to share and change free software--to make sure the software is free
for all its users. This General Public License applies to most of the Free
Software Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by the GNU
Lesser General Public License instead.) You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom to
distribute copies of free software (and charge for this service if you wish),
that you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs; and that you know you
can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of the
software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for
a fee, you must give the recipients all the rights that you have. You must make
sure that they, too, receive or can get the source code. And you must show them
these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If the
software is modified by someone else and passed on, we want its recipients to
know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish
to avoid the danger that redistributors of a free program will individually
obtain patent licenses, in effect making the program proprietary. To prevent
this, we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
## TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms of
this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or
translated into another language. (Hereinafter, translation is included without
limitation in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered by
this License; they are outside its scope. The act of running the Program is not
restricted, and the output from the Program is covered only if its contents
constitute a work based on the Program (independent of having been made by
running the Program). Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as
you receive it, in any medium, provided that you conspicuously and appropriately
publish on each copy an appropriate copyright notice and disclaimer of warranty;
keep intact all the notices that refer to this License and to the absence of any
warranty; and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at
your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus
forming a work based on the Program, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all of
these conditions:
a) You must cause the modified files to carry prominent notices stating that
you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or in
part contains or is derived from the Program or any part thereof, to be
licensed as a whole at no charge to all third parties under the terms of this
License.
c) If the modified program normally reads commands interactively when run, you
must cause it, when started running for such interactive use in the most
ordinary way, to print or display an announcement including an appropriate
copyright notice and a notice that there is no warranty (or else, saying
that you provide a warranty) and that users may redistribute the program
under these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but does not
normally print such an announcement, your work based on the Program is not
required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Program, the distribution of the whole must be on
the terms of this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise the
right to control the distribution of derivative or collective works based on
the Program.
In addition, mere aggregation of another work not based on the Program with the
Program (or with a work based on the Program) on a volume of a storage or
distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under
Section 2) in object code or executable form under the terms of Sections 1
and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code,
which must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give
any third party, for a charge no more than your cost of physically
performing source distribution, a complete machine-readable copy of the
corresponding source code, to be distributed under the terms of Sections 1
and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute
corresponding source code. (This alternative is allowed only for
noncommercial distribution and only if you received the program in object
code or executable form with such an offer, in accord with Subsection b
above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all the
source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and installation
of the executable. However, as a special exception, the source code distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
If distribution of executable or object code is made by offering access to
copy from a designated place, then offering equivalent access to copy the
source code from the same place counts as distribution of the source code,
even though third parties are not compelled to copy the source along with the
object code.
4. You may not copy, modify, sublicense, or distribute the Program except as
expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically
terminate your rights under this License. However, parties who have
received copies, or rights, from you under this License will not have their
licenses terminated so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it.
However, nothing else grants you permission to modify or distribute the
Program or its derivative works. These actions are prohibited by law if
you do not accept this License. Therefore, by modifying or distributing
the Program (or any work based on the Program), you indicate your
acceptance of this License to do so, and all its terms and conditions
for copying, distributing or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor
to copy, distribute or modify the Program subject to these terms and
conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are
imposed on you (whether by court order, agreement or otherwise) that
contradict the conditions of this License, they do not excuse you from the
conditions of this License. If you cannot distribute so as to satisfy
simultaneously your obligations under this License and any other pertinent
obligations, then as a consequence you may not distribute the Program at
all. For example, if a patent license would not permit royalty-free
redistribution of the Program by all those who receive copies directly or
indirectly through you, then the only way you could satisfy both it and
this License would be to refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system, which is implemented by public license
practices. Many people have made generous contributions to the wide range
of software distributed through that system in reliance on consistent
application of that system; it is up to the author/donor to decide if he or
she is willing to distribute software through any other system and a
licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original
copyright holder who places the Program under this License may add an
explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of the
General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published
by the Free Software Foundation. If the Program does not specify a version
number of this License, you may choose any version ever published by the
Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make
exceptions for this. Our decision will be guided by the two goals of
preserving the free status of all derivatives of our free software and of
promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO
THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
END OF TERMS AND CONDITIONS

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: java $JVM_OPTS -cp target/uberjar/youyesyet.jar clojure.main -m youyesyet.core

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# youyesyet
A web-app intended to be used by canvassers campaigning for a 'Yes' vote in the second independence referendum.
The web-app will be delivered to canvassers out knocking doors primarily through an HTML5/React single-page app; it's possible that someone else may do an Android of iPhone native app to address the same back end but at present I have no plans for this.
There must also be an administrative interface through which privileged users can set the system up and authorise canvassers, and a 'followup' interface through which issue-expert specialist canvassers can address particular electors' queries.
generated using Luminus version "2.9.11.05"
## Prerequisites
You will need [Leiningen][1] 2.0 or above installed. The database required must be [Postgres][2].
[1]: https://github.com/technomancy/leiningen
[2]: https://www.postgresql.org/
## Running
To start a web server for the application, run:
lein run
## License
Copyright © 2016 Simon Brooke for the Radical Independence Campaign.
Licensed under the GNU General Public License, version 2.0 or (at your option) any later version.

BIN
dummies/mapview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

4874
dummies/mapview.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 350 KiB

BIN
dummies/mapview.xcf Normal file

Binary file not shown.

BIN
dummies/mapview_800.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

BIN
dummies/mapview_800.xcf Normal file

Binary file not shown.

BIN
dummies/occupants.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

454
dummies/occupants.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 23 KiB

BIN
dummies/occupants.xcf Normal file

Binary file not shown.

BIN
dummies/occupants_800.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
dummies/occupants_800.xcf Normal file

Binary file not shown.

16
env/dev/clj/user.clj vendored Normal file
View file

@ -0,0 +1,16 @@
(ns user
(:require [mount.core :as mount]
[youyesyet.figwheel :refer [start-fw stop-fw cljs]]
youyesyet.core))
(defn start []
(mount/start-without #'youyesyet.core/repl-server))
(defn stop []
(mount/stop-except #'youyesyet.core/repl-server))
(defn restart []
(stop)
(start))

60
env/dev/clj/youyesyet/core.clj vendored Normal file
View file

@ -0,0 +1,60 @@
(ns youyesyet.core
(:require [youyesyet.handler :as handler]
[luminus.repl-server :as repl]
[luminus.http-server :as http]
[luminus-migrations.core :as migrations]
[youyesyet.config :refer [env]]
[clojure.tools.cli :refer [parse-opts]]
[clojure.tools.logging :as log]
[mount.core :as mount])
(:gen-class))
(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))
(mount/defstate ^{:on-reload :noop}
repl-server
:start
(when-let [nrepl-port (env :nrepl-port)]
(repl/start {:port nrepl-port}))
:stop
(when repl-server
(repl/stop repl-server)))
(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)))

View file

@ -0,0 +1,10 @@
(ns youyesyet.dev-middleware
(:require [ring.middleware.reload :refer [wrap-reload]]
[selmer.middleware :refer [wrap-error-page]]
[prone.middleware :refer [wrap-exceptions]]))
(defn wrap-dev [handler]
(-> handler
wrap-reload
wrap-error-page
wrap-exceptions))

14
env/dev/clj/youyesyet/env.clj vendored Normal file
View file

@ -0,0 +1,14 @@
(ns youyesyet.env
(:require [selmer.parser :as parser]
[clojure.tools.logging :as log]
[youyesyet.dev-middleware :refer [wrap-dev]]))
(def defaults
{:init
(fn []
(parser/cache-off!)
(log/info "\n-=[youyesyet started successfully using the development profile]=-"))
:stop
(fn []
(log/info "\n-=[youyesyet has shut down successfully]=-"))
:middleware wrap-dev})

12
env/dev/clj/youyesyet/figwheel.clj vendored Normal file
View file

@ -0,0 +1,12 @@
(ns youyesyet.figwheel
(:require [figwheel-sidecar.repl-api :as ra]))
(defn start-fw []
(ra/start-figwheel!))
(defn stop-fw []
(ra/stop-figwheel!))
(defn cljs []
(ra/cljs-repl))

14
env/dev/cljs/youyesyet/dev.cljs vendored Normal file
View file

@ -0,0 +1,14 @@
(ns ^:figwheel-no-load youyesyet.app
(:require [youyesyet.core :as core]
[devtools.core :as devtools]
[figwheel.client :as figwheel :include-macros true]))
(enable-console-print!)
(figwheel/watch-and-reload
:websocket-url "ws://localhost:3449/figwheel-ws"
:on-jsload core/mount-components)
(devtools/install!)
(core/init!)

4
env/dev/resources/config.edn vendored Normal file
View file

@ -0,0 +1,4 @@
{:dev true
:port 3000
;; when :nrepl-port is set the application starts the nREPL server on load
:nrepl-port 7000}

51
env/dev/resources/logback.xml vendored Normal file
View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<charset>UTF-8</charset>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/youyesyet.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/youyesyet.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- keep 30 days of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
</encoder>
</appender>
<logger name="org.apache.http" level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FILE"/>
</logger>
<logger name="org.xnio.nio" level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FILE"/>
</logger>
<logger name="com.zaxxer.hikari" level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FILE"/>
</logger>
<logger name="com.mchange" level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FILE"/>
</logger>
<logger name="org.eclipse.jetty" level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FILE"/>
</logger>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>

11
env/prod/clj/youyesyet/env.clj vendored Normal file
View file

@ -0,0 +1,11 @@
(ns youyesyet.env
(:require [clojure.tools.logging :as log]))
(def defaults
{:init
(fn []
(log/info "\n-=[youyesyet started successfully]=-"))
:stop
(fn []
(log/info "\n-=[youyesyet has shut down successfully]=-"))
:middleware identity})

7
env/prod/cljs/youyesyet/prod.cljs vendored Normal file
View file

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

2
env/prod/resources/config.edn vendored Normal file
View file

@ -0,0 +1,2 @@
{:production true
:port 3000}

37
env/prod/resources/logback.xml vendored Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/youyesyet.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/youyesyet.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- keep 30 days of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
</encoder>
</appender>
<logger name="org.apache.http" level="warn">
<AppenderRef ref="FILE"/>
</logger>
<logger name="org.xnio.nio" level="warn">
<AppenderRef ref="FILE"/>
</logger>
<logger name="com.zaxxer.hikari" level="warn">
<AppenderRef ref="FILE"/>
</logger>
<logger name="com.mchange" level="warn">
<AppenderRef ref="FILE"/>
</logger>
<logger name="org.eclipse.jetty" level="warn">
<AppenderRef ref="FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>

3
env/test/resources/config.edn vendored Normal file
View file

@ -0,0 +1,3 @@
{:test true
:port 3001
:nrepl-port 7001} ;; when :nrepl-port is set the application starts the nREPL server on load

149
project.clj Normal file
View file

@ -0,0 +1,149 @@
(defproject youyesyet "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[ring/ring-servlet "1.4.0"]
[clj-oauth "1.5.4"]
[ch.qos.logback/logback-classic "1.1.7"]
[re-frame "0.8.0"]
[cljs-ajax "0.5.8"]
[secretary "1.2.3"]
[reagent-utils "0.2.0"]
[reagent "0.6.0"]
[org.clojure/clojurescript "1.9.229" :scope "provided"]
[org.clojure/clojure "1.8.0"]
[korma "0.4.0"]
[selmer "1.0.9"]
[markdown-clj "0.9.89"]
[ring-middleware-format "0.7.0"]
[metosin/ring-http-response "0.8.0"]
[bouncer "1.0.0"]
[org.webjars/bootstrap "4.0.0-alpha.3"]
[org.webjars/font-awesome "4.6.3"]
[org.webjars.bower/tether "1.3.7"]
[org.clojure/tools.logging "0.3.1"]
[compojure "1.5.1"]
[ring-webjars "0.1.1"]
[ring/ring-defaults "0.2.1"]
[luminus/ring-ttl-session "0.3.1"]
[mount "0.1.10"]
[cprop "0.1.9"]
[org.clojure/tools.cli "0.3.5"]
[luminus-nrepl "0.1.4"]
[luminus-migrations "0.2.7"]
[conman "0.6.1"]
[org.postgresql/postgresql "9.4.1211"]
]
: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"]
:resource-paths ["resources" "target/cljsbuild"]
:target-path "target/%s/"
:main youyesyet.core
:migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")}
:plugins [[lein-cprop "1.0.1"]
[migratus-lein "0.4.2"]
[org.clojars.punkisdead/lein-cucumber "1.0.5"]
[lein-cljsbuild "1.1.4"]
[lein-uberwar "0.2.0"]]
:cucumber-feature-paths ["test/clj/features"]
: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]}
: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"]
:optimizations :advanced
:pretty-print false
:closure-warnings
{:externs-validation :off :non-standard-jsdoc :off}}}}}
:aot :all
:uberjar-name "youyesyet.jar"
:source-paths ["env/prod/clj"]
:resource-paths ["env/prod/resources"]}
:dev [:project/dev :profiles/dev]
:test [:project/dev :project/test :profiles/test]
:project/dev {:dependencies [[prone "1.1.2"]
[ring/ring-mock "0.3.0"]
[ring/ring-devel "1.5.0"]
[luminus-jetty "0.1.4"]
[pjstadig/humane-test-output "0.8.1"]
[org.clojure/core.cache "0.6.3"]
[org.apache.httpcomponents/httpcore "4.4"]
[clj-webdriver/clj-webdriver "0.7.2"]
[org.seleniumhq.selenium/selenium-server "2.48.2"]
[doo "0.1.7"]
[binaryage/devtools "0.8.2"]
[figwheel-sidecar "0.5.8"]
[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}}}}
: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}}}}
}
:profiles/dev {}
:profiles/test {}})

35
resources/docs/docs.md Normal file
View file

@ -0,0 +1,35 @@
<div class="bs-callout bs-callout-danger">
### 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.
</div>
### 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)

View file

@ -0,0 +1 @@
DROP TABLE users;

View file

@ -0,0 +1,9 @@
CREATE TABLE users
(id VARCHAR(20) PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
email VARCHAR(30),
admin BOOLEAN,
last_login TIME,
is_active BOOLEAN,
pass VARCHAR(300));

View file

@ -0,0 +1,68 @@
html,
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
height: 100%;
}
.navbar {
margin-bottom: 10px;
}
.navbar-brand {
float: none;
}
.navbar-nav .nav-item {
float: none;
}
.navbar-divider,
.navbar-nav .nav-item+.nav-item,
.navbar-nav .nav-link + .nav-link {
margin-left: 0;
}
@media (min-width: 34em) {
.navbar-brand {
float: left;
}
.navbar-nav .nav-item {
float: left;
}
.navbar-divider,
.navbar-nav .nav-item+.nav-item,
.navbar-nav .nav-link + .nav-link {
margin-left: 1rem;
}
}
@-moz-keyframes three-quarters-loader {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes three-quarters-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes three-quarters-loader {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View file

21
resources/sql/queries.sql Normal file
View file

@ -0,0 +1,21 @@
-- :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)
-- :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 get-user :? :1
-- :doc retrieve a user given the id.
SELECT * FROM users
WHERE id = :id
-- :name delete-user! :! :n
-- :doc delete a user given the id
DELETE FROM users
WHERE id = :id

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<title>Something bad happened</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% style "/assets/bootstrap/css/bootstrap.min.css" %}
{% style "/assets/bootstrap/css/bootstrap-theme.min.css" %}
<style type="text/css">
html {
height: 100%;
min-height: 100%;
min-width: 100%;
overflow: hidden;
width: 100%;
}
html body {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
html .container-fluid {
display: table;
height: 100%;
padding: 0;
width: 100%;
}
html .row-fluid {
display: table-cell;
height: 100%;
vertical-align: middle;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row-fluid">
<div class="col-lg-12">
<div class="centering text-center">
<div class="text-center">
<h1><span class="text-danger">Error: {{status}}</span></h1>
<hr>
{% if title %}
<h2 class="without-margin">{{title}}</h2>
{% endif %}
{% if message %}
<h4 class="text-danger">{{message}}</h4>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to youyesyet</title>
</head>
<body>
<div id="app">
<div class="container-fluid">
<div class="card-deck">
<div class="card-block">
<h4>Welcome to youyesyet</h4>
<p>If you're seeing this message, that means you haven't yet compiled your ClojureScript!</p>
<p>Please run <code>lein figwheel</code> to start the ClojureScript compiler and reload the page.</p>
<h4>For better ClojureScript development experience in Chrome follow these steps:</h4>
<ul>
<li>Open DevTools
<li>Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console)
<li>Check-in "Enable custom formatters"
<li>Close DevTools
<li>Open DevTools
</ul>
<p>See <a href="http://www.luminusweb.net/docs/clojurescript.md">ClojureScript</a> documentation for further details.</p>
</div>
</div>
</div>
</div>
<!-- scripts and styles -->
{% style "/assets/bootstrap/css/bootstrap.min.css" %}
{% style "/assets/font-awesome/css/font-awesome.min.css" %}
{% style "/css/screen.css" %}
<script type="text/javascript">
var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}";
</script>
{% script "/js/app.js" %}
</body>
</html>

View file

@ -0,0 +1,10 @@
(ns youyesyet.config
(:require [cprop.core :refer [load-config]]
[cprop.source :as source]
[mount.core :refer [args defstate]]))
(defstate env :start (load-config
:merge
[(args)
(source/from-system-props)
(source/from-env)]))

View file

@ -0,0 +1,71 @@
(ns youyesyet.db.core
(:require
[cheshire.core :refer [generate-string parse-string]]
[clojure.java.jdbc :as jdbc]
[conman.core :as conman]
[youyesyet.config :refer [env]]
[mount.core :refer [defstate]])
(:import org.postgresql.util.PGobject
java.sql.Array
clojure.lang.IPersistentMap
clojure.lang.IPersistentVector
[java.sql
BatchUpdateException
Date
Timestamp
PreparedStatement]))
(defstate ^:dynamic *db*
:start (conman/connect! {:jdbc-url (env :database-url)})
:stop (conman/disconnect! *db*))
(conman/bind-connection *db* "sql/queries.sql")
(defn to-date [^java.sql.Date sql-date]
(-> sql-date (.getTime) (java.util.Date.)))
(extend-protocol jdbc/IResultSetReadColumn
Date
(result-set-read-column [v _ _] (to-date v))
Timestamp
(result-set-read-column [v _ _] (to-date v))
Array
(result-set-read-column [v _ _] (vec (.getArray v)))
PGobject
(result-set-read-column [pgobj _metadata _index]
(let [type (.getType pgobj)
value (.getValue pgobj)]
(case type
"json" (parse-string value true)
"jsonb" (parse-string value true)
"citext" (str value)
value))))
(extend-type java.util.Date
jdbc/ISQLParameter
(set-parameter [v ^PreparedStatement stmt ^long idx]
(.setTimestamp stmt idx (Timestamp. (.getTime v)))))
(defn to-pg-json [value]
(doto (PGobject.)
(.setType "jsonb")
(.setValue (generate-string value))))
(extend-type clojure.lang.IPersistentVector
jdbc/ISQLParameter
(set-parameter [v ^java.sql.PreparedStatement stmt ^long idx]
(let [conn (.getConnection stmt)
meta (.getParameterMetaData stmt)
type-name (.getParameterTypeName meta idx)]
(if-let [elem-type (when (= (first type-name) \_) (apply str (rest type-name)))]
(.setObject stmt idx (.createArrayOf conn elem-type (to-array v)))
(.setObject stmt idx (to-pg-json v))))))
(extend-protocol jdbc/ISQLValue
IPersistentMap
(sql-value [value] (to-pg-json value))
IPersistentVector
(sql-value [value] (to-pg-json value)))

View file

@ -0,0 +1,279 @@
(ns youyesyet.db.schema
(:require [clojure.java.jdbc :as sql]
[korma.core :as kc]
[youyesyet.db.core :as yyydb]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; youyesyet.db.schema: database schema for youyesyet.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2016 Simon Brooke
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn create-districts-table!
"Create a table to hold the electoral districts in which electors are registered.
Note that, as this app is being developed for the independence referendum in which
polling is across the whole of Scotland, this part of the design isn't fully thought
through; if later adapted to general or local elections, some breakdown or hierarchy
of polling districts into constituencies will be required."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:districts
[;; it may be necessary to have a serial abstract primary key but I suspect
;; polling districts already have numbers assigned by the Electoral Commission and
;; it would be sensible to use those. TODO: check.
[:id "integer not null primary key"]
[:name "varchar(64) not null"]
;; TODO: it would make sense to hold polygon data for polling districts so we can reflect
;; them on the map, but I haven't thought through how to do that yet.
])))
(kc/defentity district
(pk :id)
(table :districts)
(database yyydb/*db*)
(entity-fields :id :name))
(defn create-addresses-table!
"Create a table to hold the addresses at which electors are registered."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:addresses
[[:id "serial not null primary key"]
;; we do NOT want to hold multiple address records for the same household. When we receive
;; the electoral roll data the addresses are likely to be text fields inlined in the elector
;; record; in digesting the roll data we need to split these out and resolve them against existing
;; addresses in the table, creating a new address record only if there's no match.
[:address "varchar(256) not null unique"]
[:postcode "varchar(16)"]
[:phone "varchar(16)"]
;; the electoral district within which this address exists
[:district "integer references districts(id)"]
[:latitude :real]
[:longitude :real]])))
(kc/defentity address
(pk :id)
(table :addresses)
(database yyydb/*db*)
(entity-fields :id :address :postcode :phone :district :latitude :longitude))
(defn create-authority-table!
"Create a table to hold the oauth authorities against which we with authenticate canvassers."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:authority
[[:id "varchar(32) not null primary key"]
;; more stuff here when I understand more
])))
(kc/defentity authority
(pk :id)
(table :authorities)
(database yyydb/*db*)
(entity-fields :id :authority))
(defn create-electors-table!
"Create a table to hold electors data."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:electors
[[:rollno "integer primary key"]
[:name "varchar(64) not null"]
[:address "integer not null references addresses(id)" ]
[:phone "varchar(16)"]
;; we'll probably only capture email data on electors if they request a followup
;; on a particular issue by email.
[:email "varchar(128)"]])))
(kc/defentity elector
(pk :id)
(table :districts)
(database yyydb/*db*)
(entity-fields :id :name))
(defn create-canvassers-table!
"Create a table to hold data on canvassers (including authentication data)."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:canvassers
[[:username "varchar(32) not null primary key"]
[:fullname "varchar(64) not null"]
;; most canvassers will be electors, we should link them:
[:elector "integer references electors(rollno) on delete no action"]
;; but some canvassers may not be electors, so we need contact details separately:
[:address "integer not null references addresses(id)" ]
[:phone "varchar(16)"]
[:email "varchar(128)"]
;; with which authority do we authenticate this canvasser? I do not want to hold even
;; encrypted passwords locally
[:authority "varchar(32) not null references authority(id) on delete no action"]
;; true if the canvasser is authorised to use the app; else false. This allows us to
;; block canvassers we suspect of misbehaving.
[:authorised :boolean]])))
(defn create-visit-table!
"Create a table to record visits by canvassers to addresses (including virtual visits by telephone)."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:visits
[[:id "serial not null primary key"]
[:canvasser "varchar(32) references canvassers(username) not null"]
[:date "timestamp with timezone not null default now()"]])))
(defn create-option-table!
"Create a table to record options in the vote. This app is being created for the Independence
referendum, which will have just two options, 'Yes' and 'No', but it might later be adapted
for more general political canvassing."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:options
[[:option "varchar(32) not null primary key"
;; To do elections you probably need party and candidate and stuff here, but
;; for the referendum it's unnecessary.
]])))
(defn create-option-district-table!
"Create a table to link options to the districts in which they are relevant. This is extremely
simple for the referendum: both options are relevant to all districts. This table is essentially
'for later expansion'."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:optionsdistricts
[[:option "varchar(32) not null references options(option)"]
[:district "integer not null references districts(id)"]])))
(defn create-opinion-table!
"Create a table to record the opinion of an elector as solicited by a canvasser during a visit.
TODO: decide whether to insert a record in this table for 'don't knows'."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:opinions
[[:id "serial primary key"]
;; the elector who gave this opinion
[:elector "integer not null references electors(rollno)"]
;; the option the elector said they were planning to vote for
[:option "varchar(32) not null references options(option)"]
[:visit "integer not null references visits(id)"]])))
(defn create-issues-table!
"A table for issues we predict electors may raise on the doorstep, for which we may be
able to provide extra information or arrange for issue-specialists to phone and talk
to the elector."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:issues
[;; short name of this issue, e.g. 'currency', 'defence', 'pensions'
[:issue "varchar(32) not null primary key"]
;; URL of some brief material the canvasser can use on the doorstap
[:url "varchar(256)"]])))
(defn create-followup-method-table!
"Create a table to hold reference data on followup methods."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followupmethod
[[:method "varchar(32) not null primary key"]])))
(defn create-issue-expertise-table!
"A table to record which canvassers have expertise in which issues, so that followup
requests can be directed to the right canvassers."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:issueexpertise
[[:expert "varchar(32) not null references canvasser(username)"]
[:issue "varchar(32) not null references issues(issue)"]
;; the method by which this expert can respond to electors on this issue
[:method "varchar 32 not null references followupmethod(method)"]])))
(defn create-followup-request-table!
"Create a table to record requests for followup contacts on particular issues."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followuprequest
[[:id "serial primary key"]
[:elector "integer not null references electors(rollno)"]
[:visit "integer not null references visits(id)"]
[:issue "varchar(32) not null references issues(issue)"]
;; We probably need a followupmethod (telephone, email, postal) and, for telephone,
;; convenient times but I haven't thought through how to represent this or how
;; the user interface will work.
[:method "varchar(32) not null references followupmethod(method)"]])))
(defn create-followup-action-table!
"Create a table to record actions on followup requests. Record in this table are almost
certainly created through a desktop-style interface rather than through te app, so it's
reasonable that there should be narrative fields."
[]
(sql/db-do-commands
yyydb/*db*
(sql/create-table-ddl
:followupaction
[[:id "serial primary key"]
[:request "integer not null references followuprequest(id)"]
[:actor "varchar(32) not null references canvassers(username)"]
[:date "timestamp with timezone not null default now()"]
[:notes "text"]
;; true if this action closes the request
[:closed :boolean]])))

View file

@ -0,0 +1,47 @@
(ns youyesyet.handler
(:require [compojure.core :refer [routes wrap-routes]]
[youyesyet.layout :refer [error-page]]
[youyesyet.routes.home :refer [home-routes]]
[youyesyet.routes.oauth :refer [oauth-routes]]
[compojure.route :as route]
[youyesyet.env :refer [defaults]]
[mount.core :as mount]
[youyesyet.middleware :as middleware]
[clojure.tools.logging :as log]
[youyesyet.config :refer [env]]))
(mount/defstate init-app
:start ((or (:init defaults) identity))
:stop ((or (:stop defaults) identity)))
(defn init
"init will be called once when
app is deployed as a servlet on
an app server such as Tomcat
put any initialization code here"
[]
(doseq [component (:started (mount/start))]
(log/info component "started")))
(defn destroy
"destroy will be called when your application
shuts down, put any clean up code here"
[]
(doseq [component (:stopped (mount/stop))]
(log/info component "stopped"))
(shutdown-agents)
(log/info "youyesyet has shut down!"))
(def app-routes
(routes
(-> #'home-routes
(wrap-routes middleware/wrap-csrf)
(wrap-routes middleware/wrap-formats))
#'oauth-routes
(route/not-found
(:body
(error-page {:status 404
:title "page not found"})))))
(def app (middleware/wrap-base #'app-routes))

View file

@ -0,0 +1,39 @@
(ns youyesyet.layout
(:require [selmer.parser :as parser]
[selmer.filters :as filters]
[markdown.core :refer [md-to-html-string]]
[ring.util.http-response :refer [content-type ok]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]))
(declare ^:dynamic *app-context*)
(parser/set-resource-path! (clojure.java.io/resource "templates"))
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
(defn render
"renders the HTML template located relative to resources/templates"
[template & [params]]
(content-type
(ok
(parser/render-file
template
(assoc params
:page template
:csrf-token *anti-forgery-token*
:servlet-context *app-context*)))
"text/html; charset=utf-8"))
(defn error-page
"error-details should be a map containing the following keys:
:status - error status
:title - error title (optional)
:message - detailed error message (optional)
returns a response map with the error page as the body
and the status specified by the status key"
[error-details]
{:status (:status error-details)
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (parser/render-file "error.html" error-details)})

View file

@ -0,0 +1,63 @@
(ns youyesyet.middleware
(:require [youyesyet.env :refer [defaults]]
[clojure.tools.logging :as log]
[youyesyet.layout :refer [*app-context* error-page]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.webjars :refer [wrap-webjars]]
[ring.middleware.format :refer [wrap-restful-format]]
[youyesyet.config :refer [env]]
[ring-ttl-session.core :refer [ttl-memory-store]]
[ring.middleware.defaults :refer [site-defaults wrap-defaults]])
(:import [javax.servlet ServletContext]))
(defn wrap-context [handler]
(fn [request]
(binding [*app-context*
(if-let [context (:servlet-context request)]
;; If we're not inside a servlet environment
;; (for example when using mock requests), then
;; .getContextPath might not exist
(try (.getContextPath ^ServletContext context)
(catch IllegalArgumentException _ context))
;; if the context is not specified in the request
;; we check if one has been specified in the environment
;; instead
(:app-context env))]
(handler request))))
(defn wrap-internal-error [handler]
(fn [req]
(try
(handler req)
(catch Throwable t
(log/error t)
(error-page {:status 500
:title "Something very bad has happened!"
:message "We've dispatched a team of highly trained gnomes to take care of the problem."})))))
(defn wrap-csrf [handler]
(wrap-anti-forgery
handler
{:error-response
(error-page
{:status 403
:title "Invalid anti-forgery token"})}))
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
handler
{:formats [:json-kw :transit-json :transit-msgpack]})]
(fn [request]
;; disable wrap-formats for websockets
;; since they're not compatible with this middleware
((if (:websocket? request) handler wrapped) request))))
(defn wrap-base [handler]
(-> ((:middleware defaults) handler)
wrap-webjars
(wrap-defaults
(-> site-defaults
(assoc-in [:security :anti-forgery] false)
(assoc-in [:session :store] (ttl-memory-store (* 60 30)))))
wrap-context
wrap-internal-error))

View file

@ -0,0 +1,35 @@
(ns youyesyet.oauth
(:require [youyesyet.config :refer [env]]
[oauth.client :as oauth]
[mount.core :refer [defstate]]
[clojure.tools.logging :as log]))
(defstate consumer
:start (oauth/make-consumer
(env :oauth-consumer-key)
(env :oauth-consumer-secret)
(env :request-token-uri)
(env :access-token-uri)
(env :authorize-uri)
:hmac-sha1))
(defn oauth-callback-uri
"Generates the oauth request callback URI"
[{:keys [headers]}]
(str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/twitter-callback"))
(defn fetch-request-token
"Fetches a request token."
[request]
(let [callback-uri (oauth-callback-uri request)]
(log/info "Fetching request token using callback-uri" callback-uri)
(oauth/request-token consumer (oauth-callback-uri request))))
(defn fetch-access-token
[request_token]
(oauth/access-token consumer request_token (:oauth_verifier request_token)))
(defn auth-redirect-uri
"Gets the URI the user should be redirected to when authenticating."
[request-token]
(str (oauth/user-approval-uri consumer request-token)))

View file

@ -0,0 +1,15 @@
(ns youyesyet.routes.home
(:require [youyesyet.layout :as layout]
[youyesyet.db.core :as db-core]
[compojure.core :refer [defroutes GET]]
[ring.util.http-response :as response]
[clojure.java.io :as io]))
(defn home-page []
(layout/render "home.html"))
(defroutes home-routes
(GET "/" [] (home-page))
(GET "/docs" [] (-> (response/ok (-> "docs/docs.md" io/resource slurp))
(response/header "Content-Type" "text/plain; charset=utf-8"))))

View file

@ -0,0 +1,32 @@
(ns youyesyet.routes.oauth
(:require [compojure.core :refer [defroutes GET]]
[ring.util.http-response :refer [ok found]]
[clojure.java.io :as io]
[youyesyet.oauth :as oauth]
[clojure.tools.logging :as log]))
(defn oauth-init
"Initiates the Twitter OAuth"
[request]
(-> (oauth/fetch-request-token request)
:oauth_token
oauth/auth-redirect-uri
found))
(defn oauth-callback
"Handles the callback from Twitter."
[request_token {:keys [session]}]
; oauth request was denied by user
(if (:denied request_token)
(-> (found "/")
(assoc :flash {:denied true}))
; fetch the request token and do anything else you wanna do if not denied.
(let [{:keys [user_id screen_name]} (oauth/fetch-access-token request_token)]
(log/info "successfully authenticated as" user_id screen_name)
(-> (found "/")
(assoc :session
(assoc session :user-id user_id :screen-name screen_name))))))
(defroutes oauth-routes
(GET "/oauth/oauth-init" req (oauth-init req))
(GET "/oauth/oauth-callback" [& req_token :as req] (oauth-callback req_token req)))

View file

@ -0,0 +1,3 @@
(ns youyesyet.validation
(:require [bouncer.core :as b]
[bouncer.validators :as v]))

View file

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

View file

@ -0,0 +1,95 @@
(ns youyesyet.core
(:require [reagent.core :as r]
[re-frame.core :as rf]
[secretary.core :as secretary]
[goog.events :as events]
[goog.history.EventType :as HistoryEventType]
[markdown.core :refer [md->html]]
[ajax.core :refer [GET POST]]
[youyesyet.ajax :refer [load-interceptors!]]
[youyesyet.handlers]
[youyesyet.subscriptions])
(:import goog.History))
(defn nav-link [uri title page collapsed?]
(let [selected-page (rf/subscribe [:page])]
[:li.nav-item
{:class (when (= page @selected-page) "active")}
[:a.nav-link
{:href uri
:on-click #(reset! collapsed? true)} title]]))
(defn navbar []
(r/with-let [collapsed? (r/atom true)]
[:nav.navbar.navbar-light.bg-faded
[:button.navbar-toggler.hidden-sm-up
{:on-click #(swap! collapsed? not)} "☰"]
[:div.collapse.navbar-toggleable-xs
(when-not @collapsed? {:class "in"})
[:a.navbar-brand {:href "#/"} "youyesyet"]
[:ul.nav.navbar-nav
[nav-link "#/" "Home" :home collapsed?]
[nav-link "#/about" "About" :about collapsed?]]]]))
(defn about-page []
[:div.container
[:div.row
[:div.col-md-12
"this is the story of youyesyet... work in progress"]]])
(defn home-page []
[:div.container
[:div.jumbotron
[:h1 "Welcome to youyesyet"]
[:p "Time to start building your site!"]
[:p [:a.btn.btn-primary.btn-lg {:href "http://luminusweb.net"} "Learn more »"]]]
(when-let [docs @(rf/subscribe [:docs])]
[:div.row
[:div.col-md-12
[:div {:dangerouslySetInnerHTML
{:__html (md->html docs)}}]]])])
(def pages
{:home #'home-page
:about #'about-page})
(defn page []
[:div
[navbar]
[(pages @(rf/subscribe [:page]))]])
;; -------------------------
;; Routes
(secretary/set-config! :prefix "#")
(secretary/defroute "/" []
(rf/dispatch [:set-active-page :home]))
(secretary/defroute "/about" []
(rf/dispatch [:set-active-page :about]))
;; -------------------------
;; History
;; must be called after routes have been defined
(defn hook-browser-navigation! []
(doto (History.)
(events/listen
HistoryEventType/NAVIGATE
(fn [event]
(secretary/dispatch! (.-token event))))
(.setEnabled true)))
;; -------------------------
;; Initialize app
(defn fetch-docs! []
(GET (str js/context "/docs") {:handler #(rf/dispatch [:set-docs %])}))
(defn mount-components []
(r/render [#'page] (.getElementById js/document "app")))
(defn init! []
(rf/dispatch-sync [:initialize-db])
(load-interceptors!)
(fetch-docs!)
(hook-browser-navigation!)
(mount-components))

View file

@ -0,0 +1,4 @@
(ns youyesyet.db)
(def default-db
{:page :home})

View file

@ -0,0 +1,18 @@
(ns youyesyet.handlers
(:require [youyesyet.db :as db]
[re-frame.core :refer [dispatch reg-event-db]]))
(reg-event-db
:initialize-db
(fn [_ _]
db/default-db))
(reg-event-db
:set-active-page
(fn [db [_ page]]
(assoc db :page page)))
(reg-event-db
:set-docs
(fn [db [_ docs]]
(assoc db :docs docs)))

View file

@ -0,0 +1,12 @@
(ns youyesyet.subscriptions
(:require [re-frame.core :refer [reg-sub]]))
(reg-sub
:page
(fn [db _]
(:page db)))
(reg-sub
:docs
(fn [db _]
(:docs db)))

View file

@ -0,0 +1,7 @@
Feature: Cukes
An example of testing a compojure app with cucumber.
Scenario: Index Page
Given I am at the "homepage"
Then I should see "Welcome to youyesyet"

View file

@ -0,0 +1,11 @@
(require '[clj-webdriver.taxi :as taxi]
'[youyesyet.browser :refer [browser-up browser-down]]
'[clojure.test :refer :all])
(Given #"^I am at the \"homepage\"$" []
(browser-up)
(taxi/to "http://localhost:3000/"))
(Then #"^I should see \"([^\"]*)\"$" [title]
(is (= (taxi/text "div.jumbotron > h1") title))
(browser-down))

View file

@ -0,0 +1,17 @@
(ns youyesyet.browser
(:require [clj-webdriver.taxi :refer :all]))
(def ^:private browser-count (atom 0))
(defn browser-up
"Start up a browser if it's not already started."
[]
(when (= 1 (swap! browser-count inc))
(set-driver! {:browser :firefox})
(implicit-wait 60000)))
(defn browser-down
"If this is the last request, shut the browser down."
[& {:keys [force] :or {force false}}]
(when (zero? (swap! browser-count (if force (constantly 0) dec)))
(quit)))

View file

@ -0,0 +1,36 @@
(ns youyesyet.test.db.core
(:require [youyesyet.db.core :refer [*db*] :as db]
[luminus-migrations.core :as migrations]
[clojure.test :refer :all]
[clojure.java.jdbc :as jdbc]
[youyesyet.config :refer [env]]
[mount.core :as mount]))
(use-fixtures
:once
(fn [f]
(mount/start
#'youyesyet.config/env
#'youyesyet.db.core/*db*)
(migrations/migrate ["migrate"] (select-keys env [:database-url]))
(f)))
(deftest test-users
(jdbc/with-db-transaction [t-conn *db*]
(jdbc/db-set-rollback-only! t-conn)
(is (= 1 (db/create-user!
t-conn
{:id "1"
:first_name "Sam"
:last_name "Smith"
:email "sam.smith@example.com"
:pass "pass"})))
(is (= {:id "1"
:first_name "Sam"
:last_name "Smith"
:email "sam.smith@example.com"
:pass "pass"
:admin nil
:last_login nil
:is_active nil}
(db/get-user t-conn {:id "1"})))))

View file

@ -0,0 +1,13 @@
(ns youyesyet.test.handler
(:require [clojure.test :refer :all]
[ring.mock.request :refer :all]
[youyesyet.handler :refer :all]))
(deftest test-app
(testing "main route"
(let [response (app (request :get "/"))]
(is (= 200 (:status response)))))
(testing "not-found route"
(let [response (app (request :get "/invalid"))]
(is (= 404 (:status response))))))

View file

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

View file

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