Starting to get the project set up. Nothing is even nearly complete yet.
This commit is contained in:
commit
b6a24bc1ce
28
Capstanfile
Normal file
28
Capstanfile
Normal 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
8
Dockerfile
Normal 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
258
LICENSE
Normal 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
1
Procfile
Normal file
|
@ -0,0 +1 @@
|
|||
web: java $JVM_OPTS -cp target/uberjar/youyesyet.jar clojure.main -m youyesyet.core
|
28
README.md
Normal file
28
README.md
Normal 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
BIN
dummies/mapview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 MiB |
4874
dummies/mapview.svg
Normal file
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
BIN
dummies/mapview.xcf
Normal file
Binary file not shown.
BIN
dummies/mapview_800.png
Normal file
BIN
dummies/mapview_800.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 KiB |
BIN
dummies/mapview_800.xcf
Normal file
BIN
dummies/mapview_800.xcf
Normal file
Binary file not shown.
BIN
dummies/occupants.png
Normal file
BIN
dummies/occupants.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 359 KiB |
454
dummies/occupants.svg
Normal file
454
dummies/occupants.svg
Normal 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
BIN
dummies/occupants.xcf
Normal file
Binary file not shown.
BIN
dummies/occupants_800.png
Normal file
BIN
dummies/occupants_800.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
BIN
dummies/occupants_800.xcf
Normal file
BIN
dummies/occupants_800.xcf
Normal file
Binary file not shown.
16
env/dev/clj/user.clj
vendored
Normal file
16
env/dev/clj/user.clj
vendored
Normal 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
60
env/dev/clj/youyesyet/core.clj
vendored
Normal 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)))
|
||||
|
10
env/dev/clj/youyesyet/dev_middleware.clj
vendored
Normal file
10
env/dev/clj/youyesyet/dev_middleware.clj
vendored
Normal 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
14
env/dev/clj/youyesyet/env.clj
vendored
Normal 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
12
env/dev/clj/youyesyet/figwheel.clj
vendored
Normal 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
14
env/dev/cljs/youyesyet/dev.cljs
vendored
Normal 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
4
env/dev/resources/config.edn
vendored
Normal 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
51
env/dev/resources/logback.xml
vendored
Normal 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
11
env/prod/clj/youyesyet/env.clj
vendored
Normal 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
7
env/prod/cljs/youyesyet/prod.cljs
vendored
Normal 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
2
env/prod/resources/config.edn
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{:production true
|
||||
:port 3000}
|
37
env/prod/resources/logback.xml
vendored
Normal file
37
env/prod/resources/logback.xml
vendored
Normal 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
3
env/test/resources/config.edn
vendored
Normal 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
149
project.clj
Normal 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
35
resources/docs/docs.md
Normal 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)
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE users;
|
|
@ -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));
|
68
resources/public/css/screen.css
Normal file
68
resources/public/css/screen.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
0
resources/public/favicon.ico
Normal file
0
resources/public/favicon.ico
Normal file
21
resources/sql/queries.sql
Normal file
21
resources/sql/queries.sql
Normal 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
|
56
resources/templates/error.html
Normal file
56
resources/templates/error.html
Normal 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>
|
46
resources/templates/home.html
Normal file
46
resources/templates/home.html
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
10
src/clj/youyesyet/config.clj
Normal file
10
src/clj/youyesyet/config.clj
Normal 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)]))
|
71
src/clj/youyesyet/db/core.clj
Normal file
71
src/clj/youyesyet/db/core.clj
Normal 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)))
|
279
src/clj/youyesyet/db/schema.clj
Normal file
279
src/clj/youyesyet/db/schema.clj
Normal 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]])))
|
||||
|
47
src/clj/youyesyet/handler.clj
Normal file
47
src/clj/youyesyet/handler.clj
Normal 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))
|
39
src/clj/youyesyet/layout.clj
Normal file
39
src/clj/youyesyet/layout.clj
Normal 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)})
|
63
src/clj/youyesyet/middleware.clj
Normal file
63
src/clj/youyesyet/middleware.clj
Normal 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))
|
35
src/clj/youyesyet/oauth.clj
Normal file
35
src/clj/youyesyet/oauth.clj
Normal 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)))
|
15
src/clj/youyesyet/routes/home.clj
Normal file
15
src/clj/youyesyet/routes/home.clj
Normal 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"))))
|
||||
|
32
src/clj/youyesyet/routes/oauth.clj
Normal file
32
src/clj/youyesyet/routes/oauth.clj
Normal 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)))
|
3
src/cljc/youyesyet/validation.cljc
Normal file
3
src/cljc/youyesyet/validation.cljc
Normal file
|
@ -0,0 +1,3 @@
|
|||
(ns youyesyet.validation
|
||||
(:require [bouncer.core :as b]
|
||||
[bouncer.validators :as v]))
|
20
src/cljs/youyesyet/ajax.cljs
Normal file
20
src/cljs/youyesyet/ajax.cljs
Normal 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})))
|
||||
|
||||
|
95
src/cljs/youyesyet/core.cljs
Normal file
95
src/cljs/youyesyet/core.cljs
Normal 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))
|
4
src/cljs/youyesyet/db.cljs
Normal file
4
src/cljs/youyesyet/db.cljs
Normal file
|
@ -0,0 +1,4 @@
|
|||
(ns youyesyet.db)
|
||||
|
||||
(def default-db
|
||||
{:page :home})
|
18
src/cljs/youyesyet/handlers.cljs
Normal file
18
src/cljs/youyesyet/handlers.cljs
Normal 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)))
|
12
src/cljs/youyesyet/subscriptions.cljs
Normal file
12
src/cljs/youyesyet/subscriptions.cljs
Normal 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)))
|
7
test/clj/features/index_page.feature
Normal file
7
test/clj/features/index_page.feature
Normal 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"
|
||||
|
11
test/clj/features/step_definitions/home_page_steps.clj
Normal file
11
test/clj/features/step_definitions/home_page_steps.clj
Normal 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))
|
17
test/clj/youyesyet/browser.clj
Normal file
17
test/clj/youyesyet/browser.clj
Normal 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)))
|
36
test/clj/youyesyet/test/db/core.clj
Normal file
36
test/clj/youyesyet/test/db/core.clj
Normal 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"})))))
|
13
test/clj/youyesyet/test/handler.clj
Normal file
13
test/clj/youyesyet/test/handler.clj
Normal 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))))))
|
8
test/cljs/youyesyet/core_test.cljs
Normal file
8
test/cljs/youyesyet/core_test.cljs
Normal 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)))
|
||||
|
6
test/cljs/youyesyet/doo_runner.cljs
Normal file
6
test/cljs/youyesyet/doo_runner.cljs
Normal file
|
@ -0,0 +1,6 @@
|
|||
(ns youyesyet.doo-runner
|
||||
(:require [doo.runner :refer-macros [doo-tests]]
|
||||
[youyesyet.core-test]))
|
||||
|
||||
(doo-tests 'youyesyet.core-test)
|
||||
|
Loading…
Reference in a new issue