├── .bowerrc ├── .gitignore ├── .jscsrc ├── .jshintrc ├── Dockerfile-mysql ├── Dockerfile-web-app ├── Gruntfile.js ├── LICENSE ├── README.md ├── Schema.sql ├── UBUNTU.md ├── bower.json ├── docker-compose.yml ├── grunt ├── concat.js ├── copy.js ├── jade.js ├── jscs.js ├── jshint.js ├── less.js ├── pleeease.js ├── replace.js ├── uglify.js └── watch.js ├── package.json ├── public_html ├── Default.html ├── api │ ├── add-custom-entry.php │ ├── add-entries.php │ ├── add-user.php │ ├── bbi-ballot.php │ ├── bbi-names.php │ ├── check-user.php │ ├── codes.php │ ├── config_sample.php │ ├── create-graph.php │ ├── delete-ballot.php │ ├── delete-entries.php │ ├── delete-users.php │ ├── delete-vote.php │ ├── delete-votes.php │ ├── duplicate-ballot.php │ ├── get-ballots.php │ ├── get-candidates.php │ ├── get-key-ballot.php │ ├── get-settings.php │ ├── get-votes.php │ ├── login.php │ ├── mysql-calls.php │ ├── new-ballot.php │ ├── no-mysql │ │ ├── add-entries.php │ │ ├── add-user.php │ │ ├── config_sample.php │ │ ├── delete-ballot.php │ │ ├── delete-entries.php │ │ ├── delete-votes.php │ │ ├── get-ballots.php │ │ ├── get-candidates.php │ │ ├── get-key-ballot.php │ │ ├── get-votes.php │ │ ├── new-ballot.php │ │ ├── update-ballot.php │ │ └── vote.php │ ├── php_errorlog │ ├── rcvis_new.php │ ├── rcvis_notes.php │ ├── rcvis_patch.php │ ├── rcvis_slug.php │ ├── results.txt │ ├── sql.php │ ├── unsecure.php │ ├── update-ballot.php │ ├── validate-voter-code.php │ └── vote.php ├── apple-touch-icon.png ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── github-fork.png ├── img │ └── every-vote-counts.png ├── inc │ ├── angular.js │ ├── lib.js │ ├── main.css │ ├── main.js │ ├── main_alt.css │ ├── ranked-choices.css │ └── timezone-picker.js ├── index.html ├── results_json.html ├── secure-elections-instructions.html ├── sw.js └── terms-of-service.html └── src ├── .htaccess ├── api ├── add-entries.php ├── add-user.php ├── config_sample.php ├── delete-ballot.php ├── delete-entries.php ├── delete-votes.php ├── get-ballots.php ├── get-candidates.php ├── get-key-ballot.php ├── get-votes.php ├── new-ballot.php ├── no-mysql │ ├── add-entries.php │ ├── add-user.php │ ├── config_sample.php │ ├── delete-ballot.php │ ├── delete-entries.php │ ├── delete-votes.php │ ├── get-ballots.php │ ├── get-candidates.php │ ├── get-key-ballot.php │ ├── get-votes.php │ ├── new-ballot.php │ ├── update-ballot.php │ └── vote.php ├── update-ballot.php └── vote.php ├── apple-touch-icon.png ├── favicon.ico ├── fonts └── .gitkeep ├── github-fork.png ├── jade ├── index.jade ├── pages │ ├── about.jade │ ├── create.jade │ ├── home.jade │ ├── profile.jade │ ├── register.html │ ├── results.jade │ └── vote.jade └── partials │ ├── congrats.jade │ ├── edit.jade │ ├── footer.jade │ ├── form.jade │ ├── head.jade │ ├── mixins.jade │ ├── navbar.jade │ ├── new-entries.jade │ ├── other-nav-items.jade │ ├── shortcode.jade │ └── shuffle.jade ├── js ├── app.js ├── main.js └── services │ ├── VoteFactory.js │ └── mc.js └── less ├── all.less ├── common.less ├── elections.less ├── fair-vote.less ├── jquery-ui.less ├── main-theme.less └── pages.less /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "lib" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders to ignore 2 | bower_components 3 | node_modules/ 4 | dist/ 5 | lib 6 | 7 | # Extensions 8 | *.orig 9 | *.log 10 | 11 | # Config files 12 | /src/api/config.php 13 | .ftppass 14 | 15 | # OS / Editor Files 16 | Thumbs.db 17 | .cache 18 | .project 19 | .settings 20 | *.sublime-project 21 | *.sublime-workspace -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireSpaceBeforeKeywords": [ 12 | "else", 13 | "while", 14 | "catch" 15 | ], 16 | "requireSpaceAfterKeywords": [ 17 | "do", 18 | "for", 19 | "if", 20 | "else", 21 | "switch", 22 | "case", 23 | "try", 24 | "catch", 25 | "void", 26 | "while", 27 | "with", 28 | "return", 29 | "typeof" 30 | ], 31 | "requireSpaceBeforeBlockStatements": true, 32 | "requireParenthesesAroundIIFE": true, 33 | "requireSpacesInConditionalExpression": true, 34 | "requireSpacesInAnonymousFunctionExpression": { 35 | "beforeOpeningCurlyBrace": true 36 | }, 37 | "disallowSpacesInAnonymousFunctionExpression": { 38 | "beforeOpeningRoundBrace": true 39 | }, 40 | "requireSpacesInNamedFunctionExpression": { 41 | "beforeOpeningCurlyBrace": true 42 | }, 43 | "disallowSpacesInNamedFunctionExpression": { 44 | "beforeOpeningRoundBrace": true 45 | }, 46 | "requireSpacesInFunctionDeclaration": { 47 | "beforeOpeningCurlyBrace": true 48 | }, 49 | "disallowSpacesInFunctionDeclaration": { 50 | "beforeOpeningRoundBrace": true 51 | }, 52 | "requireSpacesInFunction": { 53 | "beforeOpeningCurlyBrace": true 54 | }, 55 | "disallowSpacesInFunction": { 56 | "beforeOpeningRoundBrace": true 57 | }, 58 | "disallowOperatorBeforeLineBreak": ["."], 59 | "disallowMultipleLineBreaks": true, 60 | "disallowSpacesInCallExpression": true, 61 | //"requireMultipleVarDecl": true, 62 | "requireBlocksOnNewline": true, 63 | "requirePaddingNewlinesBeforeKeywords": [ 64 | "if", 65 | "do", 66 | "for", 67 | "switch", 68 | "try", 69 | "void", 70 | "while", 71 | "with", 72 | "return" 73 | ], 74 | "disallowAnonymousFunctions": true, 75 | "disallowEmptyBlocks": true, 76 | "disallowSpacesInsideObjectBrackets": "all", 77 | "disallowSpacesInsideArrayBrackets": "all", 78 | //"disallowSpacesInsideParentheses": true, 79 | //"safeContextKeyword": ["me", "view", "model", "router", "component", "app"], 80 | "disallowQuotedKeysInObjects": "allButReserved", 81 | "disallowSpaceAfterObjectKeys": true, 82 | "requireSpaceBeforeObjectValues": true, 83 | "requireCommaBeforeLineBreak": true, 84 | "requireOperatorBeforeLineBreak": true, 85 | "disallowSpaceAfterPrefixUnaryOperators": true, 86 | "disallowSpaceBeforePostfixUnaryOperators": true, 87 | "requireSpaceBeforeBinaryOperators": true, 88 | "requireSpaceAfterBinaryOperators": true, 89 | "disallowMixedSpacesAndTabs": true, 90 | "disallowTrailingComma": true, 91 | "disallowKeywordsOnNewLine": ["else"], 92 | "requireCapitalizedConstructors": true, 93 | "requireDotNotation": true, 94 | "disallowYodaConditions": true, 95 | "disallowNewlineBeforeBlockStatements": true, 96 | "validateQuoteMarks": { "mark": "'", "escape": true }, 97 | "validateIndentation": "\t", 98 | "validateParameterSeparator": ", ", 99 | "disallowPaddingNewlinesInBlocks": true 100 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "camelcase": false, 4 | "newcap": true, 5 | "quotmark": "single", 6 | "browser": true, 7 | //"maxstatements": 12, 8 | //"maxparams": 3, 9 | "maxdepth": 3, 10 | "maxcomplexity": 14 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile-mysql: -------------------------------------------------------------------------------- 1 | FROM mysql 2 | 3 | ADD Schema.sql /docker-entrypoint-initdb.d 4 | 5 | EXPOSE 3306 6 | -------------------------------------------------------------------------------- /Dockerfile-web-app: -------------------------------------------------------------------------------- 1 | #TODO: probably shouldn't run as root user, but eh. 2 | FROM alpine:3.16.1 3 | 4 | # update alpine; use php7 5 | RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories &&\ 6 | apk update && apk upgrade 7 | 8 | # Install system deps 9 | RUN apk add --update \ 10 | git \ 11 | npm \ 12 | php7-mcrypt \ 13 | php7-json \ 14 | php7 \ 15 | php7-pdo_mysql &&\ 16 | npm install npm@latest -g && \ 17 | npm install -g bower grunt-cli 18 | 19 | # make app dir 20 | RUN mkdir -p /usr/src/app 21 | WORKDIR /usr/src/app 22 | 23 | # install app deps 24 | # keep this separate so we can cache. 25 | COPY package.json /usr/src/app 26 | COPY bower.json /usr/src/app 27 | COPY .bowerrc /usr/src/app 28 | RUN npm install &&\ 29 | bower install --allow-root 30 | 31 | # copy the app source over. 32 | COPY . /usr/src/app 33 | 34 | # build the app 35 | RUN grunt 36 | WORKDIR dist 37 | 38 | EXPOSE 1337 39 | CMD ["php7", "-S", "0.0.0.0:1337"] 40 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | // load in package.json to reference any data from it (like the version number) 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json') 5 | }); 6 | 7 | // Define other properties of the config object 8 | // Define the root directory for distribution 9 | var distRoot = 'dist', 10 | // Build main distribution path. NOTE: There is no slash at the end 11 | distPath = distRoot + '/inc/', 12 | // Define banner for css and js files 13 | banner = '/*!\n' + 14 | ' * <%= pkg.description %> - v<%= pkg.version %> \n' + 15 | ' * Build Date: <%= grunt.template.today("yyyy.mm.dd") %> \n' + 16 | ' * Docs: <%= pkg.homepage %> \n' + 17 | ' * Coded @ <%= pkg.author %> \n' + 18 | ' */ \n \n'; 19 | 20 | // Set distribution path to grunt config object 21 | // This should be used when defining output files 22 | // Access this config option using <%= distRoot %> 23 | grunt.config.set('distRoot', distRoot); 24 | // Access this config option using <%= distPath %> 25 | grunt.config.set('distPath', distPath); 26 | // Access this config option using <%= banner %> 27 | grunt.config.set('banner', banner); 28 | 29 | // Load Grunt plugins from the config files in the grunt/ directory 30 | grunt.loadTasks('grunt'); 31 | 32 | // default is just dev 33 | grunt.registerTask('dev', [ 34 | 'default', 35 | 'watch' 36 | ]); 37 | 38 | // default is just dev 39 | grunt.registerTask('nomysql', [ 40 | 'default', 41 | 'copy:noMySql' 42 | ]); 43 | 44 | // Register task for generating unminified output files 45 | // This is safe to run on bamboo 46 | // Note: all subtasks of the sass and autoprefixer tasks will run. This will generate a minified and unminified version of the css. 47 | // If you run this task all of the example html files and generated documentation will reference the unminified version of Edge UI's css and js 48 | grunt.registerTask('default', [ 49 | 'jade', 50 | 'less:dev', 51 | 'copy:main', 52 | 'replace:dev', 53 | 'pleeease:dev', 54 | 'concat', 55 | 'jshint' 56 | ]); 57 | 58 | // Register task for generating minified (Prod ready) output files 59 | // Note: all subtasks of the sass and autoprefixer tasks will run. This will generate a minified and unminified version of the css. 60 | // If you run this task all of the example html files and generated documentation will reference the minified version of Edge UI's css and js 61 | grunt.registerTask('prod', [ 62 | 'jade', 63 | 'less:prod', 64 | 'copy:main', 65 | 'replace:prod', 66 | 'pleeease:prod', 67 | 'concat' 68 | ]); 69 | }; 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2020 David Moritz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RCV - Ranked Choice Voting 2 | ### [RankedChoices.com](https://rankedchoices.com) 3 | This Ranked Choice Voting app is free to use, free to improve, and free to share with the general public! 4 | 5 | Here are a few advantages to RCV: 6 | * Better representation 7 | * No “Settling” 8 | * No wasted votes 9 | * Vote by preference 10 | * Great for multi-seat elections 11 | * Easy to use 12 | 13 | For a short, 1-minute explanation of RCV; check out this video: 14 | 15 | [![RCV video](https://img.youtube.com/vi/oHRPMJmzBBw/0.jpg)](https://youtu.be/oHRPMJmzBBw) 16 | 17 | Ranked Choice voting is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice. 18 | 19 | With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system. 20 | 21 | Another thing it works really well for voting for more than one position. Everyone’s vote is still counted as one vote, but if their first choice is elected, then a portion of their vote goes to second place. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom: 22 | 23 | Click here to watch 24 | 25 | [![STV animal video](https://img.youtube.com/vi/l8XOZJkozfI/0.jpg)](https://youtu.be/l8XOZJkozfI) 26 | 27 | ## Consuming 28 | This project is designed to create the files necessary to run the site from a PHP server with MySQL database (e.g. LAMP). The database can be externally located without any issue. Make sure you specify the location of the database in the *src/api/config.php* file. 29 | 30 | The project uses Nodejs and Grunt to build a *dist/* folder whose contents will be copied into the root http folder of the server. If you don't have PHP or MySQL installed, please review . 31 | 32 | The following steps assume that you have PHP, MySQL, Nodejs and Grunt-cli installed and cwd is the project root: 33 | 34 | 1) run `npm install` 35 | 2) create "src/api/config.php" from "src/api/config_sample.php" 36 | 3) input your credentials to the MySQL database in "src/api/config.php" 37 | 4) use "Schema.sql" to build the MySQL database 38 | 5) run `grunt` (if you don't have MySQL installed, run `grunt nomysql`) 39 | 6) run `cd dist/` 40 | 7) run `php -S localhost:1337` 41 | 8) go to "localhost:1337" in your browser 42 | 43 | Let me know if you have any issues! 44 | 45 | After years of editing the dist folder on the live site, this repo became very far removed from the end product. Look at the `public_html` folder to see what is currently in production. 46 | If someone would like to convert the current site back into source code, I would welcome a pull request. 47 | 48 | Good luck! 49 | 50 | ## Docker Compose Consumption 51 | 52 | The provided docker file is meant for development / getting up and running quickly. 53 | 54 | 1. create "src/api/config.php" from "src/api/config_sample.php" (make sure the MySQL location is "db" instead of "localhost") 55 | An example that should work with the default docker-compose file as written; 56 | 57 | ```php 58 | true)); 69 | } catch (PDOException $e) { 70 | die($e->getMessage()); 71 | } 72 | 73 | ?> 74 | ``` 75 | 2. Install docker and docker-compose; [Follow these instructions per your OS](https://docs.docker.com/compose/install/) 76 | 3. If you have docker compose installed cd to the project root and run `docker-compose up` 77 | 4. go to "localhost:1337" in your browser 78 | 5. profit 79 | 80 | ## Contributing 81 | 82 | If you are interested in joining the cause and contributing, I am very appreciative! One area that I would like focused efforts on is the ability for someone to register and manage their different ballots. There was work toward this efforts, but not finished. Please contact me before putting in significant effort to avoid duplicate work. Thanks! 83 | -------------------------------------------------------------------------------- /Schema.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 4.0.10.18 3 | -- https://www.phpmyadmin.net 4 | -- 5 | -- Host: localhost:3306 6 | -- Generation Time: Apr 25, 2017 at 04:45 PM 7 | -- Server version: 5.6.28-76.1-log 8 | -- PHP Version: 5.6.30 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | SET time_zone = "+00:00"; 12 | 13 | -- 14 | -- Database: `iptheate_rcv` 15 | -- 16 | CREATE DATABASE IF NOT EXISTS `rcv_db` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 17 | USE `rcv_db`; 18 | 19 | -- -------------------------------------------------------- 20 | 21 | -- 22 | -- Table structure for table `ballots` 23 | -- 24 | 25 | DROP TABLE IF EXISTS `ballots`; 26 | CREATE TABLE IF NOT EXISTS `ballots` ( 27 | `id` int(11) NOT NULL AUTO_INCREMENT, 28 | `name` varchar(64) NOT NULL, 29 | `key` varchar(16) NOT NULL, 30 | `positions` varchar(10) NOT NULL, 31 | `createdBy` varchar(64) NOT NULL, 32 | `requireSignIn` tinyint(1) NOT NULL, 33 | `tieBreak` varchar(16) NOT NULL, 34 | `maxVotes` smallint(6) NOT NULL, 35 | `voteCutoff` datetime NOT NULL, 36 | `resultsRelease` datetime NOT NULL, 37 | `timeCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 38 | PRIMARY KEY (`id`) 39 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; 40 | 41 | -- -------------------------------------------------------- 42 | 43 | -- 44 | -- Table structure for table `entries` 45 | -- 46 | 47 | DROP TABLE IF EXISTS `entries`; 48 | CREATE TABLE IF NOT EXISTS `entries` ( 49 | `ballotId` int(11) NOT NULL, 50 | `name` varchar(128) NOT NULL, 51 | `entry_id` int(11) NOT NULL AUTO_INCREMENT, 52 | PRIMARY KEY (`entry_id`) 53 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; 54 | 55 | -- -------------------------------------------------------- 56 | 57 | -- 58 | -- Table structure for table `users` 59 | -- 60 | 61 | DROP TABLE IF EXISTS `users`; 62 | CREATE TABLE IF NOT EXISTS `users` ( 63 | `id` bigint(20) NOT NULL, 64 | `name` varchar(64) NOT NULL, 65 | `email` varchar(64) NOT NULL, 66 | `image` varchar(256) NOT NULL 67 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 68 | 69 | -- -------------------------------------------------------- 70 | 71 | -- 72 | -- Table structure for table `votes` 73 | -- 74 | 75 | DROP TABLE IF EXISTS `votes`; 76 | CREATE TABLE IF NOT EXISTS `votes` ( 77 | `ballotId` int(11) NOT NULL, 78 | `vote` text NOT NULL, 79 | `ipAddress` varchar(64) NOT NULL, 80 | `vote_id` int(11) NOT NULL AUTO_INCREMENT, 81 | PRIMARY KEY (`vote_id`) 82 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; 83 | -------------------------------------------------------------------------------- /UBUNTU.md: -------------------------------------------------------------------------------- 1 | # Steps with Ubuntu 2 | **Note**: The `$` and `mysql>` portions within the commands are ***not*** part of the command, but are simply used to denote the difference between the bash prompt and the MySQL CLI. 3 | 4 | ## Install MySQL 5 | ``` 6 | $ sudo apt-get install mysql-server 7 | ``` 8 | 9 | ## Install PHP 10 | ``` 11 | $ sudo apt-get install php 12 | ``` 13 | 14 | ## Create Database 15 | The following steps are used to create a MySQL database using the MySQL CLI within Ubuntu. Run the below steps from the projects root directory where `Schema.sql` is located. 16 | 17 | ### Enter MySQL command line prompt 18 | Typing the command below will prompt you for your password after you hit enter. 19 | ``` 20 | $ mysql -u root -p 21 | ``` 22 | 23 | Type your password then hit enter. 24 | 25 | ### Create the database 26 | **Note** the difference in the command prompt. 27 | ``` 28 | mysql> source Schema.sql 29 | ``` 30 | 31 | ### Confirm database creation 32 | When executing the prior command you should have seen a series of output messages. Those were the result of creating the database, but to confirm that the database has been created, run: 33 | 34 | ``` 35 | mysql> SHOW DATABASES; 36 | ``` 37 | 38 | This should return to you something like this with our desired `rcv_db` created: 39 | ``` 40 | +--------------------+ 41 | | Database | 42 | +--------------------+ 43 | | information_schema | 44 | | mysql | 45 | | performance_schema | 46 | | rcv_db | 47 | | sys | 48 | +--------------------+ 49 | ```` 50 | 51 | Your setup within the MySQL CLI is now complete! To exit the CLI, simply type `mysql> \q` and hit Enter. 52 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "init-angular", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "angular": "1.5.3", 6 | "angular-bootstrap": "~1.2.1", 7 | "angular-ui-sortable": "~0.13.4", 8 | "bootstrap": "3.3.6", 9 | "fontawesome": "4.5.0", 10 | "jquery": "2.2.1", 11 | "jquery-ui": "1.11.4", 12 | "lodash": "4.5.1", 13 | "moment": "2.11.2", 14 | "ng-pattern-restrict": "~0.2.1", 15 | "touch-punch": "https://github.com/furf/jquery-ui-touch-punch.git", 16 | "angular-animate": "^1.5.3" 17 | }, 18 | "resolutions": { 19 | "angular": "1.5.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-mysql 7 | volumes: 8 | - db_data:/var/lib/mysql 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-superSecretPassword} 11 | web-app: 12 | links: 13 | - db:db 14 | build: 15 | context: . 16 | dockerfile: Dockerfile-web-app 17 | volumes: 18 | - ./src:/usr/src/app/src/ 19 | ports: 20 | - ${WEB_APP_PORT:-1337}:1337 21 | volumes: 22 | db_data: 23 | -------------------------------------------------------------------------------- /grunt/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * concatenate scripts together for dev 3 | */ 4 | module.exports = function exportConcat(grunt) { 5 | grunt.config.set('concat', { 6 | js: { 7 | options: { 8 | stripBanners: true, 9 | banner: '<%= banner %>', 10 | }, 11 | files: { 12 | '<%= distPath %>angular.js': [ 13 | 'lib/jquery/dist/jquery.min.js', 14 | 'lib/angular/angular.min.js' 15 | ], 16 | '<%= distPath %>lib.js': [ 17 | 'lib/jquery-ui/jquery-ui.min.js', 18 | 'lib/bootstrap/dist/js/bootstrap.min.js', 19 | 'lib/lodash/dist/lodash.min.js', 20 | 'lib/moment/min/moment.min.js', 21 | 'lib/touch-punch/jquery.ui.touch-punch.min.js', 22 | 'lib/angular-ui-sortable/sortable.min.js', 23 | 'lib/angular-animate/animate.min.js', 24 | 'lib/angular-bootstrap/ui-bootstrap-tpls.min.js', 25 | 'lib/ng-pattern-restrict/src/ng-pattern-restrict.min.js', 26 | 'src/external/**/*.js' 27 | ], 28 | '<%= distPath %>main.js': [ 29 | 'src/services/mc.js', 30 | 'src/js/app.js', 31 | 'src/services/*.js', 32 | 'src/js/**/*.js' 33 | ] 34 | } 35 | } 36 | }); 37 | 38 | grunt.loadNpmTasks('grunt-contrib-concat'); 39 | }; 40 | -------------------------------------------------------------------------------- /grunt/copy.js: -------------------------------------------------------------------------------- 1 | module.exports = function exportCopy(grunt) { 2 | grunt.config('copy', { 3 | main: { 4 | files: [ 5 | // copy all bootstrap fonts 6 | { 7 | expand: true, 8 | cwd: 'lib/bootstrap/fonts/', 9 | src: ['**'], 10 | dest: '<%= distRoot %>/fonts/' 11 | }, 12 | 13 | // copy all fontawesome fonts 14 | { 15 | expand: true, 16 | cwd: 'lib/fontawesome/fonts/', 17 | src: ['**'], 18 | dest: '<%= distRoot %>/fonts/' 19 | }, 20 | 21 | // copy all custom fonts 22 | { 23 | expand: true, 24 | cwd: 'src/fonts/', 25 | src: ['**'], 26 | dest: '<%= distRoot %>/fonts/' 27 | }, 28 | 29 | // copy all api files too 30 | { 31 | expand: true, 32 | cwd: 'src/api', 33 | src: ['**'], 34 | dest: '<%= distRoot %>/api' 35 | }, 36 | 37 | // copy favicon & apple-icon & htaccess 38 | { 39 | expand: true, 40 | cwd: 'src/', 41 | src: ['favicon.ico', 'apple-touch-icon.png', 'github-fork.png', '.htaccess'], 42 | dest: '<%= distRoot %>/' 43 | } 44 | ] 45 | }, 46 | noMySql: { 47 | files: [ 48 | // copy all no-mysql api files 49 | { 50 | expand: true, 51 | cwd: 'src/api/no-mysql', 52 | src: ['**'], 53 | dest: '<%= distRoot %>/api' 54 | } 55 | ] 56 | } 57 | }); 58 | 59 | grunt.loadNpmTasks('grunt-contrib-copy'); 60 | }; 61 | -------------------------------------------------------------------------------- /grunt/jade.js: -------------------------------------------------------------------------------- 1 | module.exports = function exportJade(grunt) { 2 | grunt.config('jade', { 3 | compile: { 4 | options: { 5 | pretty: true 6 | }, 7 | files: { 8 | 'dist/index.html': 'src/jade/index.jade' 9 | } 10 | } 11 | }); 12 | 13 | grunt.loadNpmTasks('grunt-contrib-jade'); 14 | }; 15 | -------------------------------------------------------------------------------- /grunt/jscs.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config.set('jscs', { 3 | src: 'src/**/*.js', 4 | options: { 5 | config: '.jscsrc', 6 | fix: true 7 | } 8 | }); 9 | 10 | grunt.loadNpmTasks('grunt-jscs'); 11 | }; -------------------------------------------------------------------------------- /grunt/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = function exportJshint(grunt) { 2 | grunt.config('jshint', { 3 | options: { 4 | reporter: require('jshint-stylish'), 5 | jshintrc: '.jshintrc' 6 | }, 7 | all: [ 8 | 'src/**/*.js', 9 | 'grunt/**/*.js', 10 | '!src/external/**/*' 11 | ] 12 | }); 13 | 14 | grunt.loadNpmTasks('grunt-contrib-jshint'); 15 | }; 16 | -------------------------------------------------------------------------------- /grunt/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make less into css file in the dist directory 3 | */ 4 | module.exports = function exportLess(grunt) { 5 | grunt.config('less', { 6 | dev: { 7 | files: [{ 8 | '<%= distPath %>main.css': [ 9 | 'src/less/all.less' 10 | ] 11 | }] 12 | }, 13 | prod: { 14 | options: { 15 | cleancss: true, 16 | compress: true 17 | }, 18 | files: [{ 19 | '<%= distPath %>main.css': [ 20 | 'src/less/all.less' 21 | ] 22 | }] 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-less'); 27 | }; 28 | -------------------------------------------------------------------------------- /grunt/pleeease.js: -------------------------------------------------------------------------------- 1 | module.exports = function exportPleeease(grunt) { 2 | grunt.config('pleeease', { 3 | dev: { 4 | options: { 5 | optimizers: { 6 | minifier: false 7 | } 8 | }, 9 | files: { 10 | '<%= distPath %>main.css': '<%= distPath %>main.css' 11 | } 12 | }, 13 | prod: { 14 | options: { 15 | optimizers: { 16 | minifier: true 17 | } 18 | }, 19 | files: { 20 | '<%= distPath %>main.css': '<%= distPath %>main.css' 21 | } 22 | } 23 | }); 24 | 25 | grunt.loadNpmTasks('grunt-pleeease'); 26 | }; 27 | -------------------------------------------------------------------------------- /grunt/replace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces all references for typography.com and edge css / js 3 | * 4 | * Any file ending in '.html' with the exception of files ending in 5 | * '-documentation.html' will be searched for the patterns defined below. 6 | * This module searches for every instance of a 'match' defined below that has a 7 | * @@ in front of it. For example, the two matches defined below have 8 | * placeholders in the html pages that look like. 9 | * - @@typographyUrl 10 | * - @@majorVersion 11 | * - @@edgeuiFileName 12 | * 13 | * Using typographyUrl as an example: 14 | * Every instance of @@typographyUrl is replaced with the value in replacement 15 | * option for the typographyUrl match. The replacement option for typographUrl 16 | * is set to <%= typographyFontUrl %>. This value is defined in the Gruntfile.js 17 | * file. 18 | * 19 | * @@majorVersion and @@edgeuiFileName work the same way. 20 | * 21 | * After the placeholders are replaced with the final values the html files are 22 | * moved to the distrubtion root (this is also defined in the Gruntfile.js) 23 | * 24 | * Grunt plugin documentation: https://github.com/yoniholmes/grunt-text-replace 25 | */ 26 | module.exports = function exportReplace(grunt) { 27 | grunt.config('replace', { 28 | dev: { 29 | options: { 30 | patterns: [ 31 | { 32 | match: 'javascriptReplaceFiles', 33 | replacement: '' 34 | } 35 | ] 36 | }, 37 | files: [ 38 | { 39 | expand: true, 40 | flatten: true, 41 | src: [ 42 | '<%= distRoot %>/*.html' 43 | ], 44 | dest: '<%= distRoot %>/' 45 | } 46 | ] 47 | }, 48 | prod: { 49 | options: { 50 | patterns: [ 51 | { 52 | match: 'javascriptReplaceFiles', 53 | replacement: '' 54 | } 55 | ] 56 | }, 57 | files: [ 58 | { 59 | expand: true, 60 | flatten: true, 61 | src: [ 62 | '<%= distRoot %>/*.html' 63 | ], 64 | dest: '<%= distRoot %>/' 65 | } 66 | ] 67 | } 68 | }); 69 | 70 | grunt.loadNpmTasks('grunt-replace'); 71 | }; 72 | -------------------------------------------------------------------------------- /grunt/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * concat and minify scripts 3 | */ 4 | module.exports = function exportUglify(grunt) { 5 | grunt.config.set('uglify', { 6 | prod: { 7 | options: { 8 | banner: '<%= banner %>', 9 | }, 10 | files: { 11 | '<%= distPath %>main.js': '<%= distPath %>main.js' 12 | } 13 | } 14 | }); 15 | 16 | grunt.loadNpmTasks('grunt-contrib-uglify'); 17 | }; 18 | -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = function exportWatch(grunt) { 2 | grunt.config.set('watch', { 3 | src: { 4 | files: [ 5 | 'src/**/*.*' 6 | ], 7 | tasks: [ 8 | 'dev' 9 | ] 10 | } 11 | }); 12 | 13 | grunt.loadNpmTasks('grunt-contrib-watch'); 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new-angular", 3 | "version": "0.0.1", 4 | "description": "Initualize a new Angular app", 5 | "homepage": "http://moritzcompany.com", 6 | "author": "Moritz Company", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/davidmoritz/init-angular.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/davidmoritz/init-angular/issues" 13 | }, 14 | "scripts": { 15 | "install": "bower install" 16 | }, 17 | "devDependencies": { 18 | "bower": "latest", 19 | "grunt": "latest", 20 | "grunt-cli": "latest", 21 | "grunt-contrib-concat": "latest", 22 | "grunt-contrib-copy": "latest", 23 | "grunt-contrib-jade": "latest", 24 | "grunt-contrib-jshint": "latest", 25 | "grunt-contrib-less": "latest", 26 | "grunt-contrib-uglify": "latest", 27 | "grunt-contrib-watch": "latest", 28 | "grunt-pleeease": "~0.0.5", 29 | "grunt-replace": "latest", 30 | "grunt-jscs": "latest", 31 | "jshint-stylish": "latest" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public_html/api/add-custom-entry.php: -------------------------------------------------------------------------------- 1 | prepare($checkQ); 21 | $sth->execute(); 22 | $check=$sth->fetchAll(PDO::FETCH_ASSOC); 23 | 24 | if(strlen(json_encode($check)) > 5 && !empty($entry)) { 25 | $id = $check[0]['id']; 26 | 27 | $query = " 28 | INSERT INTO 29 | entries (`ballotId`, `name`, `image`) 30 | VALUES ('$id', '$entry', ''); 31 | "; 32 | 33 | $sth = $dbh->prepare($query); 34 | $sth->execute(); 35 | 36 | // checking for blank values. 37 | $query2 = " 38 | SELECT 39 | * 40 | FROM 41 | `entries` 42 | ORDER BY 43 | entry_id DESC 44 | LIMIT 45 | 1;"; 46 | $sth = $dbh->prepare($query2); 47 | $sth->execute(); 48 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 49 | print json_encode($results); 50 | } else { 51 | echo 'None found'; 52 | } 53 | } else { 54 | echo 'Please provde id and key'; 55 | } 56 | ?> 57 | -------------------------------------------------------------------------------- /public_html/api/add-entries.php: -------------------------------------------------------------------------------- 1 | prepare($query); 41 | $sth->execute(); 42 | echo "Success"; 43 | } 44 | ?> 45 | -------------------------------------------------------------------------------- /public_html/api/add-user.php: -------------------------------------------------------------------------------- 1 | $val) { 13 | if(!in_array($key, $acceptableFields)) 14 | continue; 15 | else if(!empty($columns)) { 16 | $columns .= ", "; 17 | $values .= ", "; 18 | $update .= ", "; 19 | } 20 | $columns .= "`$key`"; 21 | $values .= "'$val'"; 22 | $update .= "`$key` = VALUES(`$key`)"; 23 | } 24 | 25 | if(!empty($columns)) { 26 | // checking for blank values. 27 | $query = " 28 | INSERT INTO 29 | users ($columns) 30 | VALUES 31 | ($values) 32 | ON DUPLICATE KEY UPDATE id=id 33 | "; 34 | $sth = $dbh->prepare($query); 35 | $sth->execute(); 36 | } else { 37 | echo "failed to supply info"; 38 | $query = " 39 | show tables;"; 40 | $sth = $dbh->prepare($query); 41 | $sth->execute(); 42 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 43 | 44 | print json_encode($results); 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /public_html/api/bbi-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 23 | $sth->execute(); 24 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 25 | 26 | print json_encode($results); 27 | } 28 | ?> 29 | -------------------------------------------------------------------------------- /public_html/api/bbi-names.php: -------------------------------------------------------------------------------- 1 | prepare($query); 15 | $sth->execute(); 16 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 17 | 18 | print json_encode($results); 19 | ?> 20 | -------------------------------------------------------------------------------- /public_html/api/check-user.php: -------------------------------------------------------------------------------- 1 | prepare($query); 16 | $sth->execute(); 17 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 18 | 19 | print json_encode($results); 20 | } 21 | ?> 22 | -------------------------------------------------------------------------------- /public_html/api/codes.php: -------------------------------------------------------------------------------- 1 | prepare($query); 14 | $sth->execute(); 15 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 16 | 17 | print json_encode($results); 18 | ?> 19 | -------------------------------------------------------------------------------- /public_html/api/config_sample.php: -------------------------------------------------------------------------------- 1 | true)); 14 | } catch (PDOException $e) { 15 | die($e->getMessage()); 16 | } 17 | 18 | ?> 19 | -------------------------------------------------------------------------------- /public_html/api/create-graph.php: -------------------------------------------------------------------------------- 1 | prepare($query); 17 | $sth->execute(); 18 | echo $query; 19 | } 20 | ?> 21 | -------------------------------------------------------------------------------- /public_html/api/delete-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query2); 26 | $sth2->execute(); 27 | echo $query2; 28 | $query = " 29 | DELETE FROM 30 | `ballots` 31 | WHERE 32 | `createdBy` = $createdBy 33 | AND 34 | `id` = $ballotId;"; 35 | $sth = $dbh->prepare($query); 36 | $sth->execute(); 37 | } else { 38 | echo "failed to supply ballotId"; 39 | } 40 | ?> 41 | -------------------------------------------------------------------------------- /public_html/api/delete-entries.php: -------------------------------------------------------------------------------- 1 | prepare($query); 26 | $sth->execute(); 27 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 28 | } else { 29 | echo "failed to supply ballotId"; 30 | } 31 | ?> 32 | -------------------------------------------------------------------------------- /public_html/api/delete-users.php: -------------------------------------------------------------------------------- 1 | prepare($query); 32 | $sth->execute(); 33 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 34 | if(count(results) = 0) { 35 | 36 | } 37 | } else { 38 | echo "failed to supply either userId, username, or password"; 39 | } 40 | ?> 41 | -------------------------------------------------------------------------------- /public_html/api/delete-vote.php: -------------------------------------------------------------------------------- 1 | prepare($query); 39 | $sth->execute(); 40 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 41 | echo $query; 42 | } else { 43 | echo "failed to supply ballotId"; 44 | } 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /public_html/api/delete-votes.php: -------------------------------------------------------------------------------- 1 | prepare($query); 34 | $sth->execute(); 35 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 36 | } else { 37 | echo "failed to supply ballotId"; 38 | } 39 | ?> 40 | -------------------------------------------------------------------------------- /public_html/api/duplicate-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 39 | $sth->execute(); 40 | echo $query; 41 | } 42 | ?> 43 | -------------------------------------------------------------------------------- /public_html/api/get-ballots.php: -------------------------------------------------------------------------------- 1 | prepare($query); 31 | $sth->execute(); 32 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 33 | 34 | if(strtolower($createdBy) == "guest") { 35 | echo "'guest' is not a valid entry"; 36 | } else { 37 | print json_encode($results); 38 | } 39 | } else { 40 | echo "failed to supply Created By"; 41 | } 42 | ?> 43 | -------------------------------------------------------------------------------- /public_html/api/get-candidates.php: -------------------------------------------------------------------------------- 1 | prepare("SET time_zone = '+0:00'"); 10 | $sth->execute(); 11 | $query = " 12 | SELECT 13 | b.key, b.name, b.positions, b.register, b.voteCutoff, b.allowCustom, e.entry_id, e.image, e.color, e.name AS 'candidate' 14 | FROM 15 | entries e 16 | JOIN 17 | ballots b 18 | ON 19 | e.ballotId = b.id 20 | WHERE 21 | b.key = '$key' 22 | $editText 23 | "; 24 | $sth = $dbh->prepare($query); 25 | $sth->execute(); 26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 27 | 28 | if(empty($results) && !$edit) 29 | echo "Either shortcode is incorrect or voting has already been cutoff."; 30 | else 31 | print json_encode($results); 32 | } else { 33 | echo "Failed to supply Shortcode"; 34 | } 35 | ?> 36 | -------------------------------------------------------------------------------- /public_html/api/get-key-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 18 | $sth->execute(); 19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 20 | print json_encode($results); 21 | } else { 22 | echo "failed to supply Key"; 23 | } 24 | ?> 25 | -------------------------------------------------------------------------------- /public_html/api/get-settings.php: -------------------------------------------------------------------------------- 1 | prepare($query); 28 | $sth->execute(); 29 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 30 | print json_encode($results); 31 | } else { 32 | $query = " 33 | SELECT 34 | * 35 | FROM 36 | `settings` 37 | WHERE 38 | `clearance` = 1"; 39 | $sth = $dbh->prepare($query); 40 | $sth->execute(); 41 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 42 | print json_encode($results); 43 | } 44 | ?> 45 | -------------------------------------------------------------------------------- /public_html/api/get-votes.php: -------------------------------------------------------------------------------- 1 | prepare($query); 20 | $sth->execute(); 21 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 22 | 23 | // $query2 = " 24 | // SELECT 25 | // name 26 | // FROM 27 | // entries 28 | // WHERE 29 | // ballotId = '$key'"; 30 | // $sth2 = $dbh->prepare($query2); 31 | // $sth2->execute(); 32 | // // THIS DOESN'T WORK YET 33 | // array_push($results, $sth->fetchAll(PDO::FETCH_ASSOC)); 34 | 35 | if(empty($results)) 36 | echo "Either shortcode is incorrect or no one has voted yet."; 37 | else 38 | print json_encode($results); 39 | } else { 40 | echo "Failed to supply key"; 41 | } 42 | ?> 43 | -------------------------------------------------------------------------------- /public_html/api/login.php: -------------------------------------------------------------------------------- 1 | prepare($query); 27 | $sth->execute(); 28 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 29 | 30 | if(empty($results)) 31 | echo "Incorrect username and/or password"; 32 | else 33 | print json_encode($results); 34 | } -------------------------------------------------------------------------------- /public_html/api/mysql-calls.php: -------------------------------------------------------------------------------- 1 | ballots.resultsRelease;"; 17 | $sth = $dbh->prepare($query); 18 | $sth->execute(); 19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 20 | print json_encode($results); 21 | ?> -------------------------------------------------------------------------------- /public_html/api/new-ballot.php: -------------------------------------------------------------------------------- 1 | prepare("SET time_zone = '+0:00'"); 71 | $sth->execute(); 72 | $query = " 73 | INSERT INTO 74 | ballots (`name`, `timeCreated`, `key`, `positions`, `createdBy`, `resultsRelease`, `voteCutoff`, `requireSignIn`, `tieBreak`, `register`, `allowCustom`, `hideNames`, `showGraph`, `maxVotes`) 75 | VALUES 76 | ('". addslashes($_POST['name']) ."', NOW(), '". addslashes($_POST['key']) ."',". $_POST['positions'] .",'". $_POST['createdBy'] ."', '". $_POST['sqlResultsRelease'] ."', '". $_POST['sqlVoteCutoff'] ."', $requireSignIn, '$tieBreak', $register, $allowCustom, $hideNames, $showGraph, $maxVotes);"; 77 | 78 | $sth = $dbh->prepare($query); 79 | // echo $query; 80 | $sth->execute(); 81 | echo $dbh->lastInsertId(); 82 | } 83 | ?> 84 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/add-entries.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/add-user.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/config_sample.php: -------------------------------------------------------------------------------- 1 | true)); 12 | } catch (PDOException $e) { 13 | die($e->getMessage()); 14 | } 15 | 16 | ?> 17 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/delete-ballot.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/delete-entries.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/delete-votes.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/get-ballots.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/get-candidates.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/get-key-ballot.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/get-votes.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/new-ballot.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/update-ballot.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/no-mysql/vote.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public_html/api/rcvis_new.php: -------------------------------------------------------------------------------- 1 | $cfile)); 14 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data')); 15 | 16 | // grab URL and pass it to the browser 17 | echo curl_exec($ch); 18 | 19 | // echo '{"slug":"basic-ballot-creation-test-1","id":5782,"movieHorizontal":null,"movieVertical":null,"movieGenerationStatus":0,"numRounds":1,"numCandidates":1,"title":"Basic Ballot creation test","jsonFile":"https://rcvjsons-prod-us-east-1.s3.amazonaws.com/jsonFile?AWSAccessKeyId=AKIA4WE7J45B2W6TDHXU&Signature=clFhsx8LbTEBNWTGXgeIHjX78Uo%3D&Expires=1691356818","owner":"https://www.rcvis.com/api/users/656/","visualizeUrl":"https://www.rcvis.com/v/basic-ballot-creation-test-1","embedUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/bar","embedSankeyUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/sankey","embedTableUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/table","oembedEndpointUrl":"https://www.rcvis.com/oembed?url=https://www.rcvis.com/v/basic-ballot-creation-test-1"}1'; 20 | 21 | // close cURL resource, and free up system resources 22 | curl_close($ch); 23 | 24 | ?> -------------------------------------------------------------------------------- /public_html/api/rcvis_notes.php: -------------------------------------------------------------------------------- 1 | $_FILES["jsonFile"]); 18 | 19 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 20 | // set URL and other appropriate options 21 | curl_setopt($ch, CURLOPT_URL, "https://www.rcvis.com/api/visualizations/5778/"); 22 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); 23 | // curl_setopt($ch, CURLOPT_POSTFIELDS, array('jsonFile' => $_FILES['jsonFile'])); 24 | curl_setopt($ch, CURLOPT_POSTFIELDS, array('jsonFile' => $cstringfile)); 25 | // curl_setopt($ch, CURLOPT_POSTFIELDS, ['format' => 'json', 'data' => $contents]); 26 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data')); 27 | // var_dump(trim(preg_replace('/\s+/', ' ', file_get_contents($_FILES['jsonFile']['tmp_name'])))); 28 | // grab URL and pass it to the browser 29 | echo curl_exec($ch); 30 | // var_dump($_REQUEST); 31 | // close cURL resource, and free up system resources 32 | curl_close($ch); 33 | 34 | ?> -------------------------------------------------------------------------------- /public_html/api/rcvis_patch.php: -------------------------------------------------------------------------------- 1 | $cfile)); 16 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data')); 17 | 18 | // grab URL and pass it to the browser 19 | echo curl_exec($ch); 20 | 21 | // close cURL resource, and free up system resources 22 | curl_close($ch); 23 | 24 | ?> -------------------------------------------------------------------------------- /public_html/api/rcvis_slug.php: -------------------------------------------------------------------------------- 1 | prepare($query); 23 | $sth->execute(); 24 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 25 | 26 | print json_encode($results); 27 | } 28 | ?> -------------------------------------------------------------------------------- /public_html/api/results.txt: -------------------------------------------------------------------------------- 1 | { "config": { "contest": "Test BBI Presidential Election RCV Straw Poll FIND PHP", "date": "2023-08-10", "jurisdiction": "BBI Straw Pole", "office": "President", "threshold": 4 }, "results": [ { "round": 1, "tally": { "D - Kennedy": "3", "R - Hurd": "1", "D - Williamson": "1", "R - DeSantis": "1", "D - Biden": "1" }, "tallyResults": [ { "eliminated": "R - Hurd", "transfers": { "D - Kennedy": "0", "D - Williamson": "1", "R - DeSantis": "0", "D - Biden": "0" } } ] }, { "round": 2, "tally": { "D - Kennedy": "3", "D - Williamson": "2", "R - DeSantis": "1", "D - Biden": "1" }, "tallyResults": [ { "eliminated": "R - DeSantis", "transfers": { "D - Kennedy": "0", "D - Williamson": "0", "D - Biden": "1" } } ] }, { "round": 3, "tally": { "D - Kennedy": "3", "D - Williamson": "2", "D - Biden": "2" }, "tallyResults": [ { "eliminated": "D - Biden", "transfers": { "D - Williamson": "1", "D - Kennedy": "0" } } ] }, { "round": 4, "tally": { "D - Williamson": "3", "D - Kennedy": "3" }, "tallyResults": [ { "eliminated": "D - Kennedy", "transfers": { "D - Williamson": "1" } } ] }, { "round": 5, "tally": { "D - Williamson": "4" }, "tallyResults": [ { "elected": "D - Williamson" } ] } ] } -------------------------------------------------------------------------------- /public_html/api/sql.php: -------------------------------------------------------------------------------- 1 | prepare($query); 21 | $sth->execute(); 22 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 23 | 24 | print json_encode($results); 25 | ?> 26 | -------------------------------------------------------------------------------- /public_html/api/unsecure.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | query($query2); 10 | $num=$mysqli->mysqli_num_rows($result);?> 11 | 12 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | 37 | 38 | 41 | 44 | 47 | 50 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public_html/api/update-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 95 | $sth->execute(); 96 | echo $query; 97 | } 98 | ?> 99 | -------------------------------------------------------------------------------- /public_html/api/validate-voter-code.php: -------------------------------------------------------------------------------- 1 | prepare($query); 25 | $sth->execute(); 26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 27 | 28 | if ($results) 29 | echo $code; 30 | } 31 | ?> 32 | -------------------------------------------------------------------------------- /public_html/api/vote.php: -------------------------------------------------------------------------------- 1 | prepare($query); 44 | $sth->execute(); 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /public_html/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/apple-touch-icon.png -------------------------------------------------------------------------------- /public_html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/favicon.ico -------------------------------------------------------------------------------- /public_html/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public_html/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public_html/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public_html/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public_html/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public_html/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public_html/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public_html/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public_html/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public_html/github-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/github-fork.png -------------------------------------------------------------------------------- /public_html/img/every-vote-counts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/img/every-vote-counts.png -------------------------------------------------------------------------------- /public_html/inc/ranked-choices.css: -------------------------------------------------------------------------------- 1 | #sortable .candidate-item { 2 | background: #eee; 3 | } 4 | @media screen and (max-width: 992px) { 5 | #sortable {max-width: 92%;} 6 | } 7 | .fork-me-on-github { 8 | position: absolute; 9 | top: 0; 10 | right: 0; 11 | border: 0; 12 | z-index: 9; 13 | } 14 | .choice-image { 15 | margin-right: 5px; 16 | max-height: 150px; 17 | max-width: 100px; 18 | float: left; 19 | } 20 | #sortable li:after { 21 | clear: both; 22 | display: block; 23 | content: ''; 24 | } 25 | -------------------------------------------------------------------------------- /public_html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Ranked Choice Free Voting App 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | Fork me on GitHub 35 |
36 |

Ranked Choice Voting App

37 |
A better way of voting
38 |
39 | 53 |
54 |
55 |
56 |

Register

57 | 149 |
150 |
151 |

152 | Login 153 |

154 | 172 |
173 |
174 |

175 | Create New Account 176 |

177 | 192 |
193 |
194 |
195 | Thank you for signing in as {{user.name}}. 196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |

RCV

204 | 205 | 206 |

Always-free RCV! A better way to vote. Whether voting for one position or multiple, ranked choice voting allows for the greatest representation of the voters. Click "About" to learn more!

Create a Ballot 207 |
208 |
209 |
210 | 215 |
216 |
217 |
218 |
219 |

About

220 |

221 | It is my belief that RCV offers several benefits: 222 |

223 |
    224 |
  • Better representation
  • 225 |
  • No “Settling”
  • 226 |
  • No wasted votes
  • 227 |
  • Vote by preference
  • 228 |
  • Great for multi-seat elections
  • 229 |
  • Easy to use
  • 230 |
231 |

Proponents of Ranked Choice voting believe this system is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice.

232 |

With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system.

233 |

Another thing RCV works really well for is implementing a fair method in voting for more multiple positions at once. Everyone’s vote is still counted as one vote, but after their first choice is elected, the unused portion of their vote (equal to the amount not absolutely necessary to get their 1st choice elected) goes to their second choice candidate. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom:

Click here to watch 234 |

235 | Feedback 236 |

237 |

238 | I welcome questions, concerns, suggestions, and accolades!
Please send an email to davidmoritz@gmail.com 239 |

240 |

241 | Secure Elections 242 |

243 |

244 | If you would like to use this site for a secure election, please review: Secure Election Instructions 245 |

246 |

247 | Support 248 |

249 |

250 | I built this free app to help raise awareness of ranked choice voting. If you would like to show your appreciation, please send contributions to paypal.me/rankedchoices. Thank you so much! 251 |

252 |

253 | Terms of Service 254 |

255 |

256 | Please view our terms of service to understand proper use of this site: Terms of Service 257 |

258 |
259 |
260 |
261 |
262 |

Edit Ballot: {{ballot.name}}

263 |
264 | 267 | 270 |
271 |
272 |
273 | 274 | {{errors.name}} 275 |
276 |
277 | 278 |
279 | 280 | 281 |
{{errors.key}}{{success.key}} 282 |
283 |
284 | 287 |
288 |
289 |
290 | 291 | {{errors.positions}} 292 |
293 |
294 | 295 |
296 |
297 | 298 | {{errors.createdBy}} 299 |
300 |
301 | 302 |
303 | 306 | 309 | 312 | 315 |
316 |
317 |
318 | 322 |
323 |
324 | 328 |
329 |
330 |

Voting Cutoff: never (check box for custom)

331 |

Voting Cutoff: {{editDate ? (ballot.voteCutoff | date: "MMM d, y") : "Today, at"}} {{ballot.voteCutoff | date: "h:mm a" }}

332 | 335 | (Current Time: {{rightNow.format('LT')}}) 336 |
337 |
338 | 339 |
340 |
341 | 342 | 343 |
344 |
345 | 348 |
349 | 350 | 351 |
352 |
353 |
354 | 359 |
360 |
361 | 364 |
365 |
366 | 367 | 368 |
369 |
370 |
371 | 372 |
373 |
374 | 375 | 376 |
377 |
378 |
379 |
380 |
381 | 382 |
383 | 386 | 389 | 392 |
393 |
394 |
395 | 398 |
399 |
Max votes per person 400 | 401 |
402 |
403 |
404 |
405 | 406 | 407 |
408 |
409 |
410 |
411 | 412 | {{errorEntry}} 413 |
414 | 415 | 416 | 417 |

Current Entry List

418 |
{{$index + 1}} {{entry}} {{images[$index]}}
419 |
420 |
421 |
422 |

Thank you for creating a ballot!

423 |

Send this url to people for voting:

424 |

{{origin}}/{{ballot.key}}

Click here to vote yourself 425 |
426 |
427 |
428 |
429 |
430 |
431 |

Please register before voting!

432 |
433 |
434 |
435 | 436 | {{errors.uniqueCode}} 437 |
438 |
439 |
440 |
441 | 442 | {{errors.zipCode}} 443 |
444 |
445 |
446 |
447 |
448 |
452 |
456 |
460 |
464 |
468 |
472 |
476 |
477 |
478 | 479 | 480 |
481 |
482 |

Ballot: {{ballot.name}}

483 |
484 |

Order by Preference (remove any undesired using )

485 |

(Drag & Drop)

486 |

{{ballot.positions}} positions available

487 |
488 |
    489 |
  • 491 | 492 | {{item.name}} 493 |
  • 494 |
495 | 496 | 497 | 498 |
499 |
500 |
501 |

Ballot: {{ballot.name}}

502 |

Thank you for voting!

503 | RESET 504 |
505 |
506 |
507 |
508 |
509 |
510 |

Vote!

511 |
512 |
513 |
514 | 515 | {{errors.shortcode}} 516 |
517 |
518 | 519 | 520 |
521 |
522 |

Ballot: {{ballot.name}}

523 |
524 | 525 |
526 | 528 |
529 |
530 |
531 | 532 |
533 |
534 | 535 | 536 | 537 | 538 |
539 |
540 |
541 |
Enter Your Name to View the Candidates.
542 |
543 |

Order by Preference (remove any undesired using )

544 |

(Drag & Drop)

545 |

{{ballot.positions}} positions available

546 |
547 |
    548 |
  • 550 | 551 | {{item.name}} 552 |
  • 553 |
554 | 555 | 556 | 557 |
558 |

Voting URL: {{origin}}/{{ballot.key}}

559 |
560 |
561 |

Ballot: {{ballot.name}}

562 |

Thank you for voting!

563 |

564 | Click here to see the results 565 |

566 |
567 | Voting URL: {{origin}}/{{ballot.key}} 568 |
569 |
570 |
571 |
572 |
573 |
574 |

Results

575 |
576 |
577 |
578 |
579 | 580 | {{errors.shortcode}} 581 |
582 |
583 | 584 | 585 |
586 |
587 |

588 | The winner (so far): {{elected[0]}} 589 |

590 |
591 |

The winners, in order of most votes (so far):

592 |
    593 |
  1. {{result}}
  2. 594 |
595 |
596 |
597 |
598 |

599 | A graph of the results will be made available after voting has been cut off. 600 |

601 |
602 |
603 |
604 |
605 | 606 |
607 |

608 | Voting URL: {{origin}}/{{shortcode}} 609 |

610 |
611 |
612 |

613 | A visual display of the results, provided by RCVis, is ready once voting is completed. 614 |

615 | 616 |
617 | {{errors.shortcode}} 618 |
619 |
620 |
621 |
622 |
623 |

Profile

624 |

Name: {{user.name}}

625 |
626 |

Current Ballots

627 |
14 | Value1 15 | 17 | Value2 18 | 20 | Value3 21 | 23 | Value4 24 | 26 | Value5 27 |
39 | 40 | 42 | 43 | 45 | 46 | 48 | 49 | 51 | 52 |
628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 649 | 650 | 651 |
ShortcodeNameVote Cutoff ({{moment.tz(moment.tz.guess()).format('z')}})Total votesAdmin Tools
{{ballot.key}}{{ballot.name}}{{ballot.voteCutoff.year() > 2100 ? 'never' : ballot.voteCutoff.tz(moment.tz.guess()).format('MMM Do YYYY, h:mm a')}}{{ballot.totalVotes}} 644 | 645 | 646 | 647 | 648 |
652 | 653 | 654 | 655 |
656 |
657 |

Detailed Results

658 |
659 |
660 |
661 | 665 | 666 | 688 | 721 | 722 | 723 | 724 | -------------------------------------------------------------------------------- /public_html/results_json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Ranked Choice Vote Calculator App 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /public_html/secure-elections-instructions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 |

Secure Elections

12 |

Thanks for stopping by! These instructions are for those who are looking for a way to make a ranked choice ballot fast, easy, inexpensive, and secure while using RankedChoices.com!

13 |

14 | I recommend registering an account first, then the organizer can easily remove any fraudulent votes. 15 |

16 |

Open Election:

17 |

18 | Everyone's vote is public. The only concern here is if a different person voted under someone else's name, it would be hard to identify who the real vote is vs the fake. 19 |

20 |
    21 |
  1. Make a list of randomly generated numbers.
  2. 22 |
  3. Assign a number to each person not showing others.
  4. 23 |
  5. Choose "Voter Names Required" from the advanced options.
  6. 24 |
  7. Instruct each voter to enter that number next to their name.
  8. 25 |
  9. Use advanced options to release results after voting is cut-off.
  10. 26 |
27 |

28 | By taking this approach, the organizer can verify that the name of the voter and their assigned number match if there are any disputes. 29 |

30 |

Blind Election:

31 |

32 | Everyone's vote is hidden from other voters, but not the organizer. The concern here is not allowing someone to anonymously vote twice. 33 |

34 |
35 | Option 1: 36 |
37 |
    38 |
  1. Choose "Voter Names Required" from the advanced options.
  2. 39 |
  3. Choose "Hide voter names when displaying results (except from owner)" from the advanced options.
  4. 40 |
41 |
42 | Option 2: 43 |
44 |
    45 |
  1. Make a list of randomly generated numbers.
  2. 46 |
  3. Assign a number to each person not showing others.
  4. 47 |
  5. Choose "Voter Names Required" from the advanced options.
  6. 48 |
  7. Use advanced options to release results after voting is cut-off.
  8. 49 |
  9. Instruct each voter to enter that number INSTEAD OF their name.
  10. 50 |
51 |

52 | By taking this approach, the organizer can verify that every vote is legitmate because if an entered number does not match the list of generated numebrs, the vote can be thrown out. 53 |

54 |

Double-Blind Election:

55 |

56 | Everyone's vote is hidden from other voters, including the organizer. The concern here is not allowing someone to anonymously vote twice nor allowing any way to trace a vote back to the voter. 57 |

58 |
    59 |
  1. Make a list of randomly generated numbers.
  2. 60 |
  3. Copy the list of randomly generated numbers and separate them (e.g. cut them out and place them into a bowl).
  4. 61 |
  5. Allow each voter to secretly receive their number without others knowing (e.g. discretely drawing from a bowl).
  6. 62 |
  7. The voter keeps this number a secret during the entire election and beyond.
  8. 63 |
  9. Choose "Voter Names Required" from the advanced options.
  10. 64 |
  11. Use advanced options to release results after voting is cut-off.
  12. 65 |
  13. Instruct each voter to enter that number INSTEAD OF their name.
  14. 66 |
67 |

68 | By taking this approach, the organizer can verify that every vote is legitmate because if an entered number does not match the list of generated numbers, the vote can be thrown out.

69 |

70 | Also, the organizer has no way of tracing a number back to a voter without that voter's concent. 71 |

72 | 73 | -------------------------------------------------------------------------------- /public_html/sw.js: -------------------------------------------------------------------------------- 1 | importScripts('/cache-polyfill.js'); 2 | 3 | self.addEventListener('install', function(e) { 4 | e.waitUntil( 5 | caches.open('airhorner').then(function(cache) { 6 | return cache.addAll([ 7 | 'index.html', 8 | 'wrap.css', 9 | 'wrap.js', 10 | 'favicon.ico', 11 | 's1.png', 12 | 's2.png', 13 | 's3.png', 14 | 'lib/', 15 | 'lib/bootstrap.min.js', 16 | 'lib/bootstrap.min.css', 17 | 'lib/jquery.slim.min.js', 18 | 'lib/tether.min.js' 19 | ]); 20 | }) 21 | ); 22 | }); 23 | 24 | self.addEventListener('fetch', function(event) { 25 | 26 | console.log(event.request.url); 27 | 28 | event.respondWith( 29 | 30 | caches.match(event.request).then(function(response) { 31 | 32 | return response || fetch(event.request); 33 | 34 | }) 35 | 36 | ); 37 | 38 | }); -------------------------------------------------------------------------------- /public_html/terms-of-service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 |

TERMS OF USE

12 |

Thanks for stopping by! These Terms of Use set out the rules for your use of this app / access to this website. Before using rankedchoices.com, you're required to read and agreed to these Terms of Use. Remember that just by using rankedchoices.com, you're automatically agreeing to all of these terms and conditions.

13 |

Basics:

14 |
    15 |
  1. These terms and conditions of use ("Terms") govern your access to and use of rankedchoices.com, including any content, functionality, and services offered on or through rankedchoices.com or its subdomains (referred to collectively as the "Website"). By accessing, viewing, or using the Website or any of its tools, you are agreeing to be bound by these Terms.
  2. 16 |
  3. If you don't agree with any term or condition outlined in these Terms, then you may not access the Website.
  4. 17 |
  5. We may change the Terms at any time in our sole discretion. All changes are effective immediately when posted, and apply to all access to and use of the Website thereafter. Your continued use of the Website following the posting of revised Terms means that you accept and agree to the changes. You're expected to check this page when you access the Website so you're aware of any changes, because they are binding on you.
  6. 18 |
  7. These Terms constitute the entire agreement between you and RankedChoices.com and replace all previous agreements under this title.
  8. 19 |
  9. The Website is offered and available to users who are at least 13 years old and in the United States.
  10. 20 |
21 |

Website:

22 |
    23 |
  1. Your use of the Website is at your sole risk. The Website is provided on an "as is" and "as available" basis.
  2. 24 |
  3. We may add to, stop, or make changes to this Website, and any service or material we provide on the Website, in our sole discretion without notice. Any new, removed, or changed features are subject to the Terms. Continued use of the Website after any such changes means you have agreed to those changes.
  4. 25 |
  5. We will not be liable if for any reason all or any part of the Website is unavailable at any time or for any period. Periodically, we may restrict access to some parts of the Website, or the entire Website, to users.
  6. 26 |
  7. You're responsible for making any arrangements necessary for you to have access to the Website, and for ensuring that all persons who access the Website through your internet connection are aware of these Terms and comply with them.
  8. 27 |
  9. To access the Website or some of the resources it offers, you may be asked to give certain registration data or other information. It is a condition of your use of the Website that the information you provide on the Website is correct, current, and complete.
  10. 28 |
  11. If you choose, or are given, a username, password, or any other piece of information as part of our security procedures, you must treat such information as confidential, and not disclose it. You also acknowledge that your account is personal to you and agree not to provide any other person with access to this Website or portions of it using your username, password, or other security information. You agree to notify us immediately of any unauthorized access to or use of your username or password or any other breach of security. You also agree to ensure that you exit from your account at the end of each session. You should use particular caution when accessing your account from a public or shared computer so that others are not able to view or record your password or other personal information. We have the right to disable any username, password, or other identifier, whether chosen by you or provided by us, at any time in our sole discretion for any or no reason, including if, in our opinion, you have violated any provision of these Terms of Use.
  12. 29 |
30 |

Prohibited Uses:

31 |

You may use the Website only for lawful purposes and in accordance with these Terms. You agree not to use the Website:

32 |
    33 |
  1. In any way that violates any applicable law or regulation (including, without limitation, any laws regarding the export of data or software to and from the US or other countries).
  2. 34 |
  3. For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content, asking for personally identifiable information, or otherwise.
  4. 35 |
  5. To transmit, or procure the sending of, any advertising or promotional material, including any "junk mail," "chain letter," "spam," or any other similar solicitation.
  6. 36 |
  7. To impersonate or attempt to impersonate someone else, or otherwise use the Website to engage in any act of deception.
  8. 37 |
  9. In any manner that could disable, overburden, damage, or impair the site or interfere with any other party's use of the Website, including their ability to engage in real time activities through the Website.
  10. 38 |
39 |

You also agree not to use any manual or automatic device or process to monitor or copy any of the material on the Website or for any other purpose not expressly authorized in the Terms, or attempt to gain unauthorized access to or to disruption of any part of the Website, or to otherwise interfere or attempt to interfere with the proper working of the Website.

40 |

We have the right (but no obligation) to screen, review, filter, change, reject, or remove any content or accounts from the Website at our sole discretion.

41 |

User Content:

42 |
    43 |
  1. The Website may contain certain interactive features (collectively, "Interactive Services") that allow users to post, publish, or share with others (hereinafter, "post") content or materials (collectively, "User Content") on or through the Website.
  2. 44 |
  3. Any User Content you post to the site will be considered non-confidential and non-proprietary. By providing any User Content on the Website, you grant us and our licensees, successors, and assigns the right to use, reproduce, modify, perform, display, distribute, and otherwise disclose to third parties any such material.
  4. 45 |
  5. You represent, warrant, and agree that you own or control all rights in and to the User Content and have the right to grant the license granted above to us.
  6. 46 |
  7. You represent, warrant, and agree that all of your User Content does and will comply with these Terms.
  8. 47 |
  9. You understand and agree that you are responsible for any User Content you submit or contribute, and you (not us), have full responsibility for such content, including its legality, reliability, accuracy, and appropriateness.
  10. 48 |
  11. We are not responsible or liable to any third party for the content or accuracy of any User Content posted by you or any other user of the Website.
  12. 49 |
50 |

Reliance on Information Posted:

51 |
    52 |
  1. The information presented on or through the Website is made available solely for general information purposes. We do not warrant the accuracy, completeness, or usefulness of this information. Any reliance you place on such information is strictly at your own risk. We disclaim all liability and responsibility arising from any reliance placed on such materials by you or any other visitor to the Website, or by anyone who may be informed of any of its contents.
  2. 53 |
  3. This Website may include content provided by third parties, including materials provided by other users. We are not responsible, or liable to you or any third party, for the content or accuracy of any materials provided by any third parties.
  4. 54 |
55 |

Geographic Restrictions:

56 |
    57 |
  1. The owner of the Website is based in the State of Iowa in the United States. We provide this Website for use only by persons located in the United States. We make no claims that the Website or any of its content is accessible or appropriate outside of the United States. Access to the Website may not be legal by certain persons or in certain countries. If you access the Website from outside the United States, you do so on your own risk and are solely responsible for compliance with local laws.
  2. 58 |
59 |

Disclaimer of Warranties:

60 |
    61 |
  1. You understand that we cannot and do not guarantee or warrant that the Website will be free of viruses or other destructive code. You are responsible for implementing sufficient procedures and checkpoints to satisfy your particular requirements for anti-virus protection and accuracy of data input and output, and for maintaining a means external to our site for any reconstruction of any lost data.
  2. 62 |
  3. We do not warrant that the Website will meet your requirements or expectations, or that the Website will be delivered timely, securely, uninterrupted, or error-free. We do not warrant that any results that may be obtained through the Website will be accurate or reliable. The Website should not be used or relied upon for critical responses or voting results.
  4. 63 |
  5. TO THE FULLEST EXTENT PROVIDED BY LAW, WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE CAUSED BY A DISTRIBUTED DENIAL-OF-SERVICE ATTACK, VIRUSES, OR OTHER TECHNOLOGICALLY HARMFUL MATERIAL THAT MAY INFECT YOUR COMPUTER EQUIPMENT, COMPUTER PROGRAMS, DATA, OR OTHER PROPRIETARY MATERIAL DUE TO YOUR USE OF THE WEBSITE OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE OR TO YOUR DOWNLOADING OF ANY MATERIAL POSTED ON IT, OR ON ANY WEBSITE LINKED TO IT.
  6. 64 |
  7. YOUR USE OF THE WEBSITE, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE IS AT YOUR OWN RISK. THE WEBSITE, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED.
  8. 65 |
  9. RANKEDCHOICES.COM MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY, OR AVAILABILITY OF THE WEBSITE.
  10. 66 |
  11. WITHOUT LIMITING THE FOREGOING, NEITHER THE COMPANY NOR ANYONE ASSOCIATED WITH THE COMPANY REPRESENTS OR WARRANTS THAT THE WEBSITE, ITS CONTENT, OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE WILL BE ACCURATE, RELIABLE, ERROR-FREE, OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT OUR SITE OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE WEBSITE OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS.
  12. 67 |
  13. TO THE FULLEST EXTENT PROVIDED BY LAW, THE COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE.
  14. 68 |
  15. THE FOREGOING DOES NOT AFFECT ANY WARRANTIES THAT CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW.
  16. 69 |
70 |

Limitation on Liability:

71 |
    72 |
  1. TO THE FULLEST EXTENT ALLOWED BY LAW, IN NO EVENT WILL THE COMPANY, ITS AFFILIATES, OR THEIR LICENSORS, SERVICE PROVIDERS, EMPLOYEES, AGENTS, OFFICERS, OR DIRECTORS BE LIABLE FOR DAMAGES OF ANY KIND, UNDER ANY LEGAL THEORY, ARISING OUT OF OR IN CONNECTION WITH YOUR USE, OR INABILITY TO USE, THE WEBSITE, ANY WEBSITES LINKED TO IT, ANY CONTENT ON THE WEBSITE OR SUCH OTHER WEBSITES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO, PERSONAL INJURY, PAIN AND SUFFERING, EMOTIONAL DISTRESS, LOSS OF REVENUE, LOSS OF PROFITS, LOSS OF BUSINESS OR ANTICIPATED SAVINGS, LOSS OF USE, LOSS OF GOODWILL, LOSS OF DATA, AND WHETHER CAUSED BY TORT (INCLUDING NEGLIGENCE), BREACH OF CONTRACT, OR OTHERWISE, EVEN IF FORESEEABLE.
  2. 73 |
74 |

Indemnification:

75 |
    76 |
  1. You agree to defend, indemnify, and hold harmless RankedChoices.com from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms of Use or your use of the Website, including, but not limited to, your User Content, any use of the Website's content or services other than as expressly authorized in these Terms , or your use of any information obtained from the Website.
  2. 77 |
78 |

Governing Law and Jurisdiction:

79 |
    80 |
  1. All matters relating to the Website and these Terms, and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of the United States and/or the State of Iowa without giving effect to any choice or conflict of law provision or rule.
  2. 81 |
  3. Any legal suit, action, or proceeding arising out of, or related to, these Terms or the Website shall be instituted exclusively in the federal courts of the United States or the courts of the State of Iowa, in each case located in the City of Des Moines and County of Polk. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.
  4. 82 |
83 | 84 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | 3 | RewriteCond %{REQUEST_FILENAME} -f 4 | RewriteRule ^.*$ - [L] 5 | 6 | RewriteRule ^/?([^/]+)/?$ #$1 [L,QSA] 7 | 8 | 9 | order allow,deny 10 | allow from all 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/api/add-entries.php: -------------------------------------------------------------------------------- 1 | prepare($query); 38 | $sth->execute(); 39 | echo "Success"; 40 | } 41 | ?> -------------------------------------------------------------------------------- /src/api/add-user.php: -------------------------------------------------------------------------------- 1 | $val) { 13 | if(!in_array($key, $acceptableFields)) 14 | continue; 15 | else if(!empty($columns)) { 16 | $columns .= ", "; 17 | $values .= ", "; 18 | $update .= ", "; 19 | } 20 | $columns .= "`$key`"; 21 | $values .= "'$val'"; 22 | $update .= "`$key` = VALUES(`$key`)"; 23 | } 24 | 25 | if(!empty($columns)) { 26 | // checking for blank values. 27 | $query = " 28 | INSERT INTO 29 | users ($columns) 30 | VALUES 31 | ($values) 32 | ON DUPLICATE KEY UPDATE 33 | $update"; 34 | $sth = $dbh->prepare($query); 35 | $sth->execute(); 36 | } else { 37 | echo "failed to supply info"; 38 | } 39 | ?> -------------------------------------------------------------------------------- /src/api/config_sample.php: -------------------------------------------------------------------------------- 1 | true)); 12 | } catch (PDOException $e) { 13 | die($e->getMessage()); 14 | } 15 | 16 | ?> 17 | -------------------------------------------------------------------------------- /src/api/delete-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 19 | $sth->execute(); 20 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 21 | } else { 22 | echo "failed to supply ballotId"; 23 | } 24 | ?> -------------------------------------------------------------------------------- /src/api/delete-entries.php: -------------------------------------------------------------------------------- 1 | prepare($query); 25 | $sth->execute(); 26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 27 | } else { 28 | echo "failed to supply ballotId"; 29 | } 30 | ?> -------------------------------------------------------------------------------- /src/api/delete-votes.php: -------------------------------------------------------------------------------- 1 | prepare($query); 26 | $sth->execute(); 27 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 28 | } else { 29 | echo "failed to supply ballotId"; 30 | } 31 | ?> -------------------------------------------------------------------------------- /src/api/get-ballots.php: -------------------------------------------------------------------------------- 1 | prepare($query); 25 | $sth->execute(); 26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 27 | 28 | if(strtolower($createdBy) == "guest") { 29 | echo "'guest' is not a valid entry"; 30 | } else { 31 | print json_encode($results); 32 | } 33 | } else { 34 | echo "failed to supply Created By"; 35 | } 36 | ?> -------------------------------------------------------------------------------- /src/api/get-candidates.php: -------------------------------------------------------------------------------- 1 | prepare($query); 21 | $sth->execute(); 22 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 23 | 24 | if(empty($results)) 25 | echo "Either shortcode is incorrect or voting has already been cutoff"; 26 | else 27 | print json_encode($results); 28 | } else { 29 | echo "Failed to supply Shortcode"; 30 | } 31 | ?> -------------------------------------------------------------------------------- /src/api/get-key-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 18 | $sth->execute(); 19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 20 | print json_encode($results); 21 | } else { 22 | echo "failed to supply Key"; 23 | } 24 | ?> -------------------------------------------------------------------------------- /src/api/get-votes.php: -------------------------------------------------------------------------------- 1 | ballots.resultsRelease;"; 21 | $sth = $dbh->prepare($query); 22 | $sth->execute(); 23 | $results=$sth->fetchAll(PDO::FETCH_ASSOC); 24 | 25 | // $query2 = " 26 | // SELECT 27 | // name 28 | // FROM 29 | // entries 30 | // WHERE 31 | // ballotId = '$key'"; 32 | // $sth2 = $dbh->prepare($query2); 33 | // $sth2->execute(); 34 | // // THIS DOESN'T WORK YET 35 | // array_push($results, $sth->fetchAll(PDO::FETCH_ASSOC)); 36 | 37 | if(empty($results)) 38 | echo "Either shortcode is incorrect or results aren't ready to be released"; 39 | else 40 | print json_encode($results); 41 | } else { 42 | echo "Failed to supply key"; 43 | } 44 | ?> -------------------------------------------------------------------------------- /src/api/new-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 57 | $sth->execute(); 58 | echo $dbh->lastInsertId(); 59 | } 60 | ?> -------------------------------------------------------------------------------- /src/api/no-mysql/add-entries.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/add-user.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/config_sample.php: -------------------------------------------------------------------------------- 1 | true)); 12 | } catch (PDOException $e) { 13 | die($e->getMessage()); 14 | } 15 | 16 | ?> 17 | -------------------------------------------------------------------------------- /src/api/no-mysql/delete-ballot.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/delete-entries.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/delete-votes.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/get-ballots.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/get-candidates.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/get-key-ballot.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/get-votes.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/new-ballot.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/update-ballot.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/no-mysql/vote.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/update-ballot.php: -------------------------------------------------------------------------------- 1 | prepare($query); 60 | $sth->execute(); 61 | echo $query; 62 | } 63 | ?> -------------------------------------------------------------------------------- /src/api/vote.php: -------------------------------------------------------------------------------- 1 | prepare($query); 36 | $sth->execute(); 37 | } 38 | ?> -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/favicon.ico -------------------------------------------------------------------------------- /src/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/fonts/.gitkeep -------------------------------------------------------------------------------- /src/github-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/github-fork.png -------------------------------------------------------------------------------- /src/jade/index.jade: -------------------------------------------------------------------------------- 1 | include partials/mixins 2 | 3 | doctype html 4 | html(ng-app='mainApp') 5 | include partials/head 6 | body(ng-cloak ng-controller='MainCtrl') 7 | a(href="https://github.com/davidmoritz/rcv") 8 | img(style="position: absolute; top: 0; right: 0; border: 0; z-index: 100;" src="github-fork.png" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png") 9 | .container 10 | h2 Ranked Choice Vote Calculator 11 | h5 A better way of voting 12 | include partials/navbar 13 | .container 14 | .panel.panel-default(ng-show="activeLink == 'register'") 15 | .panel-body 16 | include pages/register.html 17 | .panel.panel-default(ng-show="activeLink == 'home'") 18 | .panel-body 19 | include pages/home 20 | .panel.panel-default(ng-show="activeLink == 'about'") 21 | .panel-body 22 | include pages/about 23 | .panel.panel-default(ng-show="activeLink == 'create'") 24 | .panel-body 25 | include pages/create 26 | .panel.panel-default(ng-show="activeLink == 'vote'") 27 | .panel-body 28 | include pages/vote 29 | .panel.panel-default(ng-show="activeLink == 'results'") 30 | .panel-body 31 | include pages/results 32 | .panel.panel-default(ng-show="activeLink == 'profile'") 33 | .panel-body 34 | include pages/profile 35 | .panel.panel-default(ng-show="activeLink == 'results' && showText") 36 | .panel-heading 37 | h3.panel-title Detailed Results 38 | .panel-body(ng-bind-html="bodyText") 39 | include partials/footer 40 | -------------------------------------------------------------------------------- /src/jade/pages/about.jade: -------------------------------------------------------------------------------- 1 | h2 About 2 | ul 3 | li Better representation 4 | li No “Settling” 5 | li No wasted votes 6 | li Vote by preference 7 | li Great for multi-seat elections 8 | li Easy to use 9 | p. 10 | Ranked Choice voting is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice. 11 | p. 12 | With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system. 13 | p. 14 | Another thing it works really well for voting for more than one position. Everyone’s vote is still counted as one vote, but if their first choice is elected, then a portion of their vote goes to second place. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom: 15 | a(href="https://youtu.be/l8XOZJkozfI" target="_blank") Click here to watch 16 | //- .fluid-video-wrapper 17 | //- iframe(src="https://www.youtube.com/embed/l8XOZJkozfI" frameborder="0" allowfullscreen) -------------------------------------------------------------------------------- /src/jade/pages/create.jade: -------------------------------------------------------------------------------- 1 | h1 Ballot: {{ballot.name}} 2 | include ../partials/form 3 | div(ng-show="entries && !congrats") 4 | include ../partials/new-entries 5 | div(ng-show="congrats") 6 | include ../partials/congrats -------------------------------------------------------------------------------- /src/jade/pages/home.jade: -------------------------------------------------------------------------------- 1 | .row 2 | .col-sm-6.text-center 3 | h1 RCV 4 | h2.hidden-xs.hidden-sm Ranked Choice Vote 5 | h3.hidden-md.hidden-lg Ranked Choice Vote 6 | p A better way to vote. Whether voting for one position or multiple, ranked choice voting allows for the greatest representation of the voters. Click "About" to learn more! 7 | a.btn.btn-primary(href="create") Create a Ballot 8 | .col-sm-6 9 | a(href="create") 10 | img.img.img-responsive.vote-button(src="http://lwv.org/files/every_vote_counts_button_0.jpg") -------------------------------------------------------------------------------- /src/jade/pages/profile.jade: -------------------------------------------------------------------------------- 1 | h1 Profile 2 | //-img(ng-if='user.image' src='user.image') 3 | h3(ng-show='user.name') Name: {{user.name}} 4 | h3(ng-show='user.email') Email: {{user.email}} 5 | div(ng-show='allBallots.length') 6 | h3 Current Ballots 7 | table.table.ballots 8 | thead 9 | tr 10 | th Shortcode 11 | th Name 12 | th Vote Cutoff 13 | th Total votes 14 | th Admin Tools 15 | tbody 16 | tr(ng-repeat="ballot in allBallots track by ballot.id") 17 | td {{ballot.key}} 18 | td {{ballot.name}} 19 | td {{ballot.voteCutoff | date:"MMM d, y - h:mm a"}} 20 | td {{ballot.totalVotes}} 21 | td 22 | a.fa.fa-list-ol.pointer(href="/results#{{ballot.key}}" tooltip-placement="top-left" uib-tooltip="See Results") 23 | i.fa.fa-refresh.pointer(ng-click="deleteVotes(ballot)" tooltip-placement="top" uib-tooltip="Reset Votes") 24 | i.fa.fa-pencil.pointer(ng-click="editBallot(ballot)" tooltip-placement="top" uib-tooltip="Edit Ballot") 25 | i.fa.fa-trash.pointer(ng-click="deleteBallot(ballot)" tooltip-placement="top-right" uib-tooltip="Delete Ballot") -------------------------------------------------------------------------------- /src/jade/pages/register.html: -------------------------------------------------------------------------------- 1 |

Register

2 |
3 |
4 |

Google

5 |
6 | 7 | 29 | Sign out of Google 30 | 38 |
39 |
40 |

Facebook

41 | 83 | 84 | 89 | 90 | 91 | 92 |
93 |
94 |
95 |
96 |
97 | Thank you for signing in as {{user.name}}. 98 |
-------------------------------------------------------------------------------- /src/jade/pages/results.jade: -------------------------------------------------------------------------------- 1 | h1 Results 2 | span.ballot-name 3 | div(ng-hide="final") 4 | include ../partials/shortcode 5 | div(ng-show="final") 6 | h2 The winner{{elected.length == 1 ? "" : "s"}}: 7 | span(ng-repeat="result in elected") {{result}} 8 | span(ng-if="!$last") ,  9 | button.btn.btn-default.hidden-xs(ng-click="showText = !showText") {{!showText ? "Show" : "Hide"}} details -------------------------------------------------------------------------------- /src/jade/pages/vote.jade: -------------------------------------------------------------------------------- 1 | div(ng-hide='candidates') 2 | h1 Vote! 3 | include ../partials/shortcode 4 | div(ng-if='candidates' ng-hide="thanks") 5 | h1 Ballot: {{ballot.name}} 6 | h2 Order by Preference 7 | h3(ng-show='ballot.positions > 1') {{ballot.positions}} positions available 8 | form(name='voteForm' ng-submit='submitVote()') 9 | ul#sortable(ui-sortable ng-model='candidates') 10 | li(ng-repeat='item in candidates track by $index' data-id='{{item.entry_id}}') {{item.name}} 11 | i.fa.fa-close(ng-click="removeCandidate($index)") 12 | button.btn.btn-primary.clearfix(type='submit') Vote! 13 | button.btn.btn-default(type="button" ng-click='resetCandidates()') Reset 14 | div(ng-show="thanks") 15 | h1 Ballot: {{ballot.name}} 16 | h3 Thank you for voting! -------------------------------------------------------------------------------- /src/jade/partials/congrats.jade: -------------------------------------------------------------------------------- 1 | h3 Thank you for creating a ballot! 2 | h4 Send this url to people for voting: 3 | h4.text-success {{origin}}/{{ballot.key}} 4 | a(href="{{origin}}/vote#{{ballot.key}}") Click here to vote yourself -------------------------------------------------------------------------------- /src/jade/partials/edit.jade: -------------------------------------------------------------------------------- 1 | form(name='ballotForm' ng-submit='editBallot()') 2 | .form-group 3 | label Ballot Name 4 | input.form-control(name="name" ng-model="ballot.name") 5 | span.text-danger(ng-show='errors.name') {{errors.name}} 6 | .form-group 7 | label Positions to be Elected 8 | input.form-control(type="number" name="positions" ng-model="ballot.positions") 9 | span.text-danger(ng-show='errors.positions') {{errors.positions}} 10 | .form-group 11 | label Delete all votes and reset Ballot 12 | input(type="checkbox" ng-model="showDelete") 13 | input.btn.btn-danger(ng-show="showDelete" type="button" value="Are you sure?" ng-click="deleteVotes(ballot)") 14 | span.text-warning(ng-show='deleted') Votes have been deleted. 15 | .form-group 16 | h4 Voting Cutoff: {{ballot.voteCutoff | date: "MMM d, y"}} {{ballot.voteCutoff | date: "h:mm a" }} 17 | label Edit Time 18 | input(type="checkbox" ng-model="editTime") 19 | uib-timepicker(ng-show="editTime" ng-model="ballot.voteCutoff" minute-step="1") 20 | label Edit Date 21 | input(type="checkbox" ng-model="editDate") 22 | .input-group(ng-show="editDate") 23 | input.form-control(type="text" ng-focus="releaseOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.voteCutoff" is-open="releaseOpen") 24 | span.input-group-btn 25 | button.btn.btn-default(type="button" ng-click="releaseOpen = !releaseOpen") 26 | i.fa.fa-calendar 27 | .form-group 28 | label Release Results: 29 | span(ng-hide="showRelease") after voting (check box for custom) 30 | input(type="checkbox" ng-model="showRelease" ng-click="sameTime()") 31 | small.text-muted(ng-show="showRelease && ballot.resultsRelease") {{ballot.resultsRelease | date: dateFormat }} 32 | div(ng-show="showRelease") 33 | .input-group 34 | input.form-control(type="text" ng-focus="cutoffOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.resultsRelease" is-open="cutoffOpen") 35 | span.input-group-btn 36 | button.btn.btn-default(type="button" ng-click="cutoffOpen = !cutoffOpen") 37 | i.fa.fa-calendar 38 | uib-timepicker(ng-model="ballot.resultsRelease" minute-step="15") 39 | .form-group 40 | label Require Voter Sign-in: 41 | span(ng-hide="ballot.requireSignIn") (with either Google or Facebook) 42 | input(type="checkbox" ng-model="ballot.requireSignIn") 43 | div(ng-show="ballot.requireSignIn") 44 | .input-group 45 | span.input-group-addon Max votes per person 46 | input.form-control(type="number" ng-model="ballot.maxVotes") 47 | input.btn.btn-info(type="submit" value="Submit") -------------------------------------------------------------------------------- /src/jade/partials/footer.jade: -------------------------------------------------------------------------------- 1 | footer 2 | small © 2016 Ranked Choices Vote Calculator -------------------------------------------------------------------------------- /src/jade/partials/form.jade: -------------------------------------------------------------------------------- 1 | form(name='ballotForm' ng-submit='newBallot()' ng-hide='entries') 2 | .form-group 3 | label Ballot Name 4 | input.form-control(name="name" ng-model="ballot.name") 5 | span.text-danger(ng-show='errors.name') {{errors.name}} 6 | .form-group 7 | label Shortcode (for link {{origin}}/{{ballot.key}}) 8 | .input-group 9 | input.form-control(name="key" ng-model="ballot.key" ng-pattern-restrict pattern="\\w*" ng-change="checkAvailability()" maxlength=16 ng-readonly="editBallot") 10 | span.input-group-btn 11 | button.btn.btn-secondary(type="button" ng-click="generateRandomKey()" ng-disabled="editBallot") New Shortcode 12 | span.text-danger(ng-show='errors.key') {{errors.key}} 13 | span.text-success(ng-show='success.key') {{success.key}} 14 | .form-group 15 | label Advanced Options 16 | input(type="checkbox" ng-model="advancedOptions") 17 | div(ng-show="advancedOptions") 18 | .form-group 19 | label Positions to be Elected 20 | input.form-control(type="number" name="positions" min="1" ng-model="ballot.positions") 21 | span.text-danger(ng-show='errors.positions') {{errors.positions}} 22 | .form-group(ng-show="user.name") 23 | label Created By {{user.name}} 24 | .form-group(ng-show="ballot.createdBy == 'guest'") 25 | label Created By 26 | small.text-muted using default "guest" will restrict future editing. Please  27 | a.btn-link(href="register") Sign in 28 | | to allow future editing. 29 | input.form-control(name="createdBy" ng-model="ballot.createdBy" readonly) 30 | span.text-danger(ng-show='errors.createdBy') {{errors.createdBy}} 31 | .form-group 32 | h4(ng-hide="editDate || editTime") Voting Cutoff: never (check box for custom) 33 | h4(ng-show="editDate || editTime") Voting Cutoff: {{editDate ? (ballot.voteCutoff | date: "MMM d, y") : "Today, at"}} {{ballot.voteCutoff | date: "h:mm a" }} 34 | label Edit Time 35 | input(type="checkbox" ng-model="editTime") 36 | uib-timepicker(ng-show="editTime" ng-model="ballot.voteCutoff" minute-step="1") 37 | label Edit Date 38 | input(type="checkbox" ng-model="editDate") 39 | .input-group(ng-show="editDate") 40 | input.form-control(type="text" ng-focus="releaseOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.voteCutoff" is-open="releaseOpen") 41 | span.input-group-btn 42 | button.btn.btn-default(type="button" ng-click="releaseOpen = !releaseOpen") 43 | i.fa.fa-calendar 44 | .form-group 45 | label Release Results: 46 | span(ng-hide="showRelease || editDate || editTime") immediately (check box for custom) 47 | span(ng-hide="showRelease || (!editDate && !editTime)") after voting (check box for custom) 48 | input(type="checkbox" ng-model="showRelease" ng-click="sameTime()") 49 | small.text-muted(ng-show="showRelease && ballot.resultsRelease") {{ballot.resultsRelease | date: dateFormat }} 50 | div(ng-show="showRelease") 51 | .input-group 52 | input.form-control(type="text" ng-focus="cutoffOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.resultsRelease" is-open="cutoffOpen") 53 | span.input-group-btn 54 | button.btn.btn-default(type="button" ng-click="cutoffOpen = !cutoffOpen") 55 | i.fa.fa-calendar 56 | uib-timepicker(ng-model="ballot.resultsRelease" minute-step="15") 57 | .form-group 58 | label Tie-Break Method: 59 | div 60 | label.radio-inline(title="Randomly determine the order of candidates being eliminated") 61 | input(type="radio" value="random" name="tie-break-method" ng-model="ballot.tieBreak") 62 | | Random (official) 63 | label.radio-inline(title="Observe the preference of secondary choices to determine") 64 | input(type="radio" value="weighted" name="tie-break-method" ng-model="ballot.tieBreak") 65 | | Weighted 66 | label.radio-inline(title="All candidates in a tie are simultaneously eliminated") 67 | input(type="radio" value="none" name="tie-break-method" ng-model="ballot.tieBreak") 68 | | No Winner 69 | .form-group(ng-hide="true") 70 | label Require Voter Sign-in: 71 | span(ng-hide="ballot.requireSignIn") (with either Google or Facebook) 72 | input(type="checkbox" ng-model="ballot.requireSignIn") 73 | div(ng-show="ballot.requireSignIn") 74 | .input-group 75 | span.input-group-addon Max votes per person 76 | input.form-control(type="number" ng-model="ballot.maxVotes") 77 | input.btn.btn-info(type="submit" value="Submit" ng-disabled="errors.key") 78 | -------------------------------------------------------------------------------- /src/jade/partials/head.jade: -------------------------------------------------------------------------------- 1 | head 2 | script(src='https://apis.google.com/js/platform.js' async defer) 3 | meta(charset='UTF-8') 4 | meta(name='description', content='RCV Calculator App') 5 | meta(name='author', content='Moritz Company') 6 | meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no') 7 | meta(name='apple-mobile-web-app-capable' content='yes') 8 | meta(name='apple-touch-fullscreen' content='yes') 9 | meta(name='google-signin-client_id' content='828084230115-98814hb54sbrs34jjiecfirgak4svths.apps.googleusercontent.com') 10 | title Ranked Choice Vote Calculator App 11 | link(rel='shortcut icon', href='favicon.ico') 12 | link(rel='stylesheet', href='inc/main.css') 13 | script(src='inc/angular.js') 14 | script(src='inc/lib.js') 15 | script(src='inc/main.js?v=10') 16 | base(href='/') 17 | //- include analytics: 18 | 28 | -------------------------------------------------------------------------------- /src/jade/partials/mixins.jade: -------------------------------------------------------------------------------- 1 | //- all 4 sizes are passed into the mixin in case a different ratio size is provided for the various sizes 2 | mixin col(xs, ms, sm, md, lg) 3 | - items = [] 4 | - (xs > 1 && xs <= 12) ? items.push('col-xs-' + xs) : undefined 5 | - (ms > 1 && ms <= 12) ? items.push('col-ms-' + ms) : undefined 6 | - (sm > 1 && sm <= 12) ? items.push('col-sm-' + sm) : undefined 7 | - (md > 1 && md <= 12) ? items.push('col-md-' + md) : undefined 8 | - (lg > 1 && lg <= 12) ? items.push('col-lg-' + lg) : undefined 9 | - (!items.length) ? items.push('col-xs-12') : undefined 10 | section(class=items.join(' '))&attributes(attributes) 11 | if block 12 | block -------------------------------------------------------------------------------- /src/jade/partials/navbar.jade: -------------------------------------------------------------------------------- 1 | nav.navbar-fairvote.navbar(role="navigation") 2 | .container 3 | .row 4 | //- Brand and toggle get grouped for better mobile display 5 | .navbar-header 6 | button.navbar-toggle.collapsed(type="button" data-toggle="collapse" data-target="#navbar" aria-expanded="false") 7 | span.sr-only Toggle navigation 8 | span.icon-bar 9 | span.icon-bar 10 | span.icon-bar 11 | //- a.navbar-brand(href="/home") RCV 12 | //- Collect the nav links, forms, and other content for toggle 13 | #navbar.collapse.navbar-collapse 14 | ul.nav.navbar-nav 15 | li(ng-repeat="nav in navItems" ng-class="{'active': nav.link == activeLink}" ng-hide="nav.hide") 16 | a(href="/{{nav.link}}") {{nav.text}} 17 | span.sr-only(ng-if="nav.link == activeLink") (current) -------------------------------------------------------------------------------- /src/jade/partials/new-entries.jade: -------------------------------------------------------------------------------- 1 | form(name='entriesForm' ng-submit='addEntry()') 2 | .form-group 3 | label New Entry (use ENTER to add) 4 | input.form-control(name="entryInput" ng-model="entryInput") 5 | span(ng-show='errorEntry') {{errorEntry}} 6 | button.btn.btn-info(type='submit') Add Entry 7 | button.btn.btn-default(type='button' ng-click="submitEntries()") Submit All Entries 8 | h2 Current Entry List 9 | h5(ng-repeat="entry in entries track by $index") 10 | strong {{$index + 1}}  11 | | {{entry}}  12 | i.fa.fa-close.pointer(ng-click="removeEntry($index)") -------------------------------------------------------------------------------- /src/jade/partials/other-nav-items.jade: -------------------------------------------------------------------------------- 1 | form.navbar-form.navbar-left(role="search") 2 | .form-group 3 | input.form-control(type="text" placeholder="Search") 4 | button.btn.btn-default(type="submit") Submit 5 | ul.nav.navbar-nav.navbar-right 6 | li 7 | a(href="#") Link 8 | li.dropdown 9 | a.dropdown-toggle(href="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false") Dropdown 10 | span.caret 11 | ul.dropdown-menu 12 | li 13 | a(href='#') Home 14 | li 15 | a(href='#') About 16 | li 17 | a(href='#') Create a new ballot! 18 | li 19 | a(href='#') Edit Ballot! 20 | li 21 | a(href='#') Results 22 | li 23 | a(href='#') Vote! -------------------------------------------------------------------------------- /src/jade/partials/shortcode.jade: -------------------------------------------------------------------------------- 1 | form(ng-submit="submitShortcode()") 2 | .row 3 | .form-group.col-sm-6.col-md-4.col-lg-3 4 | label Enter Ballot Shortcode 5 | input.form-control(name="ballot" ng-model="shortcode") 6 | span.text-danger(ng-show='errors.shortcode') {{errors.shortcode}} 7 | input.btn.btn-success(type="submit" value="Submit") -------------------------------------------------------------------------------- /src/jade/partials/shuffle.jade: -------------------------------------------------------------------------------- 1 | div(ng-hide="thanks || congrats") 2 | div(ng-show="editBallot") 3 | select.form-control(ng-model="ballot" ng-options="item as item.name for item in allBallots track by item.id" ng-change="changeEditBallot()") 4 | h1 Ballot: {{ballot.name}} 5 | div(ng-show="createBallot") 6 | div(ng-show='updated && !candidates') 7 | include partials/new-entries 8 | div(ng-show="congrats") 9 | include partials/congrats -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | var mainApp = angular.module('mainApp', [ 2 | 'ui.sortable', 3 | 'ui.bootstrap' 4 | ]); 5 | 6 | mainApp.config(function($locationProvider) { 7 | $locationProvider.html5Mode(true); 8 | }); 9 | 10 | mainApp.run(function runWithDependencies($rootScope) { 11 | $rootScope._ = _; 12 | $rootScope.moment = moment; 13 | $rootScope.mc = mc; 14 | }); 15 | 16 | function onSignIn(googleUser) { 17 | var profile = googleUser.getBasicProfile(); 18 | console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead. 19 | console.log('Name: ' + profile.getName()); 20 | console.log('Image URL: ' + profile.getImageUrl()); 21 | console.log('Email: ' + profile.getEmail()); 22 | setUser({ 23 | id: profile.getId(), 24 | name: profile.getName(), 25 | email: profile.getEmail(), 26 | image: profile.getImageUrl() 27 | }); 28 | } 29 | 30 | window.fbAsyncInit = function() { 31 | FB.init({ 32 | appId: '916767641776363', 33 | xfbml: true, 34 | version: 'v2.5' 35 | }); 36 | FB.getLoginStatus(function(response) { 37 | statusChangeCallback(response); 38 | }); 39 | }; 40 | 41 | // (function(d, s, id){ 42 | // var js, fjs = d.getElementsByTagName(s)[0]; 43 | // if (d.getElementById(id)) {return;} 44 | // js = d.createElement(s); js.id = id; 45 | // js.src = '//connect.facebook.net/en_US/sdk.js'; 46 | // fjs.parentNode.insertBefore(js, fjs); 47 | // }(document, 'script', 'facebook-jssdk')); -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | mainApp.controller('MainCtrl', [ 2 | '$scope', 3 | '$location', 4 | '$timeout', 5 | '$interval', 6 | '$http', 7 | '$sce', 8 | 'VoteFactory', 9 | function MainCtrl($s, $loc, $timeout, $interval, $http, $sce, VF) { 10 | 'use strict'; 11 | 12 | //during development 13 | window.$s = $s; 14 | $s.user = $s.user || {}; 15 | 16 | var getVoteParam = function(param) { 17 | var temp = $loc.$$absUrl.split('/').pop(); 18 | 19 | if (!temp) { 20 | $s.navigate('home'); 21 | } else { 22 | temp = temp.split('#'); 23 | param = temp[0]; 24 | } 25 | 26 | if (_.find($s.navItems, {link: param})) { 27 | $s.navigate(param, temp[1]); 28 | 29 | return ''; 30 | } 31 | 32 | return param; 33 | }; 34 | 35 | var updateTime = function(dateObj) { 36 | var mom = moment(dateObj); 37 | 38 | if (mom.isDST()) { 39 | mom.utcOffset(-5); 40 | } else { 41 | mom.utcOffset(-6); 42 | } 43 | 44 | return mom.format('YYYY-MM-DD HH:mm:ss'); 45 | }; 46 | 47 | var deleteThis = function(data, item) { 48 | $http({ 49 | method: 'POST', 50 | url: '/api/delete-' + item + '.php', 51 | data: data, 52 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 53 | }).success(function(resp) { 54 | $s.deleted = true; 55 | }); 56 | }; 57 | 58 | var resetBallot = function() { 59 | $s.generateRandomKey(); 60 | $s.entries = null; 61 | 62 | return { 63 | positions: 1, 64 | createdBy: $s.user.id || 'guest', 65 | maxVotes: 1, 66 | tieBreak: 'random', 67 | voteCutoff: roundResultsRelease() 68 | }; 69 | }; 70 | 71 | var getBallots = function() { 72 | // we need to get ballots based on user signin 73 | if ($s.user.id) { 74 | $http({ 75 | method: 'POST', 76 | url: '/api/get-ballots.php', 77 | data: $s.user, 78 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 79 | }).then(function(resp) { 80 | $s.now = new Date(); 81 | $s.allBallots = resp.data.map(function(ballot) { 82 | ballot.voteCutoff = new Date(ballot.voteCutoff); 83 | ballot.resultsRelease = new Date(ballot.resultsRelease); 84 | 85 | return ballot; 86 | }); 87 | }); 88 | } else { 89 | setTimeout(getBallots, 500); 90 | } 91 | }; 92 | 93 | var resetNav = function(hide) { 94 | $s.navItems.map(function(item) { 95 | if (item.link == 'profile') { 96 | item.hide = !hide; 97 | } else if (item.link == 'register') { 98 | item.hide = !!hide; 99 | } 100 | }); 101 | }; 102 | 103 | $s.$watch(function() { 104 | return window.location.pathname; 105 | }, getVoteParam); 106 | 107 | // initialize scoped variables 108 | _.assign($s, { 109 | activeLink: 'home', 110 | navItems: [ 111 | { 112 | link: 'home', 113 | text: 'Home' 114 | },{ 115 | link: 'about', 116 | text: 'About' 117 | },{ 118 | link: 'create', 119 | text: 'Create a new Ballot!' 120 | },{ 121 | link: 'profile', 122 | text: 'Profile', 123 | hide: true 124 | },{ 125 | link: 'results', 126 | text: 'Results' 127 | },{ 128 | link: 'register', 129 | text: 'Register' 130 | },{ 131 | link: 'vote', 132 | text: 'Vote!' 133 | } 134 | ], 135 | ballot: {}, 136 | errors: {}, 137 | success: {}, 138 | dateFormat: 'MMM d, y h:mm a', 139 | pickerFormat: 'fullDate', 140 | pickerOptions: { 141 | showWeeks: false 142 | }, 143 | origin: window.location.origin 144 | }); 145 | 146 | _.extend($s, VF); 147 | 148 | function roundResultsRelease() { 149 | var now = new Date(); 150 | var m = now.getMinutes(); 151 | var offset = parseInt((m + 25) / 15) * 15; 152 | now = new Date(now.setSeconds(0)); 153 | 154 | // vote ends 15 minutes after it starts round to the nearest quarter 155 | return new Date(now.setMinutes(offset)); 156 | } 157 | 158 | $s.navigate = function(link, shortcode) { 159 | var title = _.find($s.navItems, {link: link}).text; 160 | $s.activeLink = link; 161 | 162 | if ($('.navbar-collapse').hasClass('in')) { 163 | $('.navbar-collapse').collapse('hide'); 164 | } 165 | 166 | switch (link) { 167 | case 'create': 168 | $s.ballot = resetBallot(); 169 | $s.congrats = false; 170 | break; 171 | case 'profile': 172 | getBallots(); 173 | } 174 | 175 | if (shortcode) { 176 | $s.shortcode = shortcode; 177 | $s.submitShortcode(); 178 | } else { 179 | $s.shortcode = ''; 180 | } 181 | }; 182 | 183 | $s.signOut = function() { 184 | var auth2 = gapi.auth2.getAuthInstance(); 185 | auth2.signOut().then(function() { 186 | console.log('User signed out.'); 187 | resetNav(); 188 | }); 189 | }; 190 | 191 | $s.sameTime = function() { 192 | $s.ballot.resultsRelease = new Date($s.ballot.voteCutoff.getTime()); 193 | }; 194 | 195 | $s.updateUser = function(user) { 196 | $s.user = user; 197 | resetNav(true); 198 | $s.$apply(); 199 | $http({ 200 | method: 'POST', 201 | url: '/api/add-user.php', 202 | data: $s.user, 203 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 204 | }); 205 | }; 206 | 207 | $s.getCandidates = function() { 208 | $http.get('/api/get-candidates.php?key=' + $s.shortcode) 209 | .then(function(resp) { 210 | if (typeof resp.data == 'string') { 211 | $s.errors.shortcode = resp.data; 212 | 213 | return; 214 | } 215 | 216 | $s.originalCandidates = resp.data.map(function(entry) { 217 | $s.ballot = entry; 218 | $s.ballot.positions = parseInt($s.ballot.positions); 219 | 220 | return { 221 | name: entry.candidate, 222 | id: entry.entry_id 223 | }; 224 | }); 225 | $s.activeLink = 'vote'; 226 | $s.resetCandidates(); 227 | }) 228 | ; 229 | }; 230 | 231 | $s.getResults = function() { 232 | var key = $s.shortcode || $s.ballot.key; 233 | $http.get('/api/get-votes.php?key=' + key) 234 | .then(function(resp) { 235 | if (typeof resp.data == 'string') { 236 | $s.errors.shortcode = resp.data; 237 | 238 | return; 239 | } 240 | 241 | $s.votes = resp.data.map(function(result) { 242 | $s.seats = parseInt(result.positions); 243 | $s.tieBreakMethod = result.tieBreakMethod; 244 | 245 | if (result.vote) { 246 | return JSON.parse(result.vote); 247 | } 248 | // THIS DOESN'T WORK YET 249 | //$s.names = result; 250 | }); 251 | $('.ballot-name').text(' for ' + resp.data[0].name); 252 | $s.names = _.uniq(_.flatten($s.votes)); 253 | $s.runTheCode(); 254 | $s.bodyText = $sce.trustAsHtml($s.outputstring); 255 | $s.final = true; 256 | }) 257 | ; 258 | }; 259 | 260 | $s.generateRandomKey = function(len) { 261 | len = len || 4; 262 | var key = Math.random().toString(36).substr(2, len); 263 | $http.get('/api/get-key-ballot.php?key=' + key) 264 | .then(function(resp) { 265 | if (resp.data.length) { 266 | $s.generateRandomKey(++len); 267 | } else { 268 | $s.errors.key = null; 269 | $s.success.key = null; 270 | $s.ballot.key = key; 271 | } 272 | }) 273 | ; 274 | }; 275 | 276 | $s.changeEditBallot = function() { 277 | $s.createBallot = true; 278 | $s.ballot.positions = parseInt($s.ballot.positions); 279 | $s.ballot.resultsRelease = new Date($s.ballot.resultsRelease); 280 | $s.ballot.voteCutoff = new Date($s.ballot.voteCutoff); 281 | 282 | $http.get('/api/get-candidates.php?key=' + $s.ballot.key) 283 | .then(function(resp) { 284 | if (resp.data) { 285 | $s.entries = resp.data.map(function(entry) { 286 | return entry.candidate; 287 | }); 288 | } 289 | }) 290 | ; 291 | }; 292 | 293 | $s.checkAvailability = _.debounce(function() { 294 | $http.get('/api/get-key-ballot.php?key=' + $s.ballot.key) 295 | .then(function(resp) { 296 | if (resp.data.length) { 297 | $s.success.key = null; 298 | 299 | if ($s.ballot.key) { 300 | $s.errors.key = $s.ballot.key + ' is already in use'; 301 | } else { 302 | $s.errors.key = 'Shortcode is required'; 303 | } 304 | } else { 305 | $s.errors.key = null; 306 | $s.success.key = $s.ballot.key + ' is available'; 307 | } 308 | }) 309 | ; 310 | }, 250); 311 | 312 | $s.removeEntry = function(idx) { 313 | $s.entries.splice(idx, 1); 314 | }; 315 | 316 | $s.removeCandidate = function(idx) { 317 | $s.candidates.splice(idx, 1); 318 | }; 319 | 320 | $s.resetCandidates = function() { 321 | $s.candidates = _.shuffle($s.originalCandidates); 322 | }; 323 | 324 | $s.newBallot = function() { 325 | if (!$s.editTime && !$s.editDate) { 326 | $s.ballot.resultsRelease = updateTime(new Date()); 327 | $s.ballot.voteCutoff = updateTime(new Date('2199-12-31T23:59:59')); 328 | } else { 329 | $s.sameTime(); 330 | $s.ballot.voteCutoff = updateTime($s.ballot.voteCutoff); 331 | } 332 | 333 | if ($s.showRelease) { 334 | $s.ballot.resultsRelease = updateTime($s.ballot.resultsRelease); 335 | } 336 | 337 | $s.ballot.createdBy = $s.user.id || 'guest'; 338 | $http({ 339 | method: 'POST', 340 | url: '/api/' + ($s.editBallot ? 'update' : 'new') + '-ballot.php', 341 | data: $s.ballot, 342 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 343 | }).success(function(resp) { 344 | if (resp.errors) { 345 | $s.errors = resp.errors; 346 | } else { 347 | if (!$s.editBallot) { 348 | $s.ballot.id = resp; 349 | } 350 | $s.entries = []; 351 | } 352 | }); 353 | }; 354 | 355 | $s.submitEntries = function() { 356 | if ($s.entries.length < 2) { 357 | $s.errorEntry = 'Must have at least 2 entries'; 358 | 359 | return; 360 | } 361 | 362 | if ($s.editBallot) { 363 | $http.get('/api/delete-entries.php?ballotId=' + $s.ballot.id) 364 | .then(function(resp) { 365 | $s.editBallot = false; 366 | $s.submitEntries(); 367 | }) 368 | ; 369 | 370 | return; 371 | } 372 | $http({ 373 | method: 'POST', 374 | url: '/api/add-entries.php', 375 | data: { 376 | entries: $s.entries, 377 | ballotId: $s.ballot.id 378 | }, 379 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 380 | }).success(function(resp) { 381 | if (resp.errors) { 382 | $s.errors = resp.errors; 383 | } else { 384 | $s.congrats = true; 385 | } 386 | }); 387 | }; 388 | 389 | $s.submitVote = function() { 390 | $http({ 391 | method: 'POST', 392 | url: '/api/vote.php', 393 | data: { 394 | vote: JSON.stringify($s.candidates.map(function(cand) { 395 | return cand.name; 396 | })), 397 | key: $s.ballot.key 398 | }, 399 | headers: {'Content-Type': 'application/x-www-form-urlencoded'} 400 | }).success(function(resp) { 401 | $s.thanks = true; 402 | console.log(resp); 403 | }); 404 | }; 405 | 406 | $s.deleteBallot = function(ballot) { 407 | if (confirm('Delete ' + ballot.name + ' ballot?\nThis action cannot be undone')) { 408 | deleteThis(ballot, 'ballot'); 409 | _.remove($s.allBallots, ballot); 410 | } 411 | }; 412 | 413 | $s.deleteVotes = function(ballot) { 414 | if (confirm('Delete all ' + ballot.name + ' votes?\nThis action cannot be undone')) { 415 | deleteThis(ballot, 'votes'); 416 | ballot.totalVotes = 0; 417 | } 418 | }; 419 | 420 | $s.voteNow = function() { 421 | $s.congrats = false; 422 | $s.originalCandidates = $s.entries; 423 | $s.resetCandidates(); 424 | }; 425 | 426 | $s.showResults = function() { 427 | $s.thanks = true; 428 | $s.final = true; 429 | $s.getResults(); 430 | }; 431 | 432 | $s.submitShortcode = function() { 433 | if ($s.activeLink == 'results') { 434 | $s.getResults(); 435 | } else { 436 | $s.getCandidates(); 437 | } 438 | }; 439 | 440 | $s.addEntry = function() { 441 | if (!$s.entryInput.length) { 442 | $s.errorEntry = 'Entries must not be blank'; 443 | } else if ($s.entries.indexOf($s.entryInput) !== -1) { 444 | $s.errorEntry = 'No duplicate entries allowed'; 445 | } else { 446 | $s.errorEntry = ''; 447 | $s.entries.push($s.entryInput); 448 | $s.entryInput = ''; 449 | } 450 | }; 451 | 452 | $s.shortcode = getVoteParam(); 453 | 454 | if ($s.shortcode) { 455 | $s.activeLink = 'vote'; 456 | $s.getCandidates(); 457 | } 458 | } 459 | ]); 460 | -------------------------------------------------------------------------------- /src/js/services/VoteFactory.js: -------------------------------------------------------------------------------- 1 | mainApp.factory('VoteFactory', [ 2 | function VoteFactory() { 3 | 'use strict'; 4 | 5 | return { 6 | wincount: 0, 7 | roundnum: 0, 8 | elected: [], 9 | 10 | runTheCode: function() { 11 | this.renewQuota(); 12 | this.outputstring = this.createHeader(); 13 | this.anotherRound(); 14 | }, 15 | 16 | renewQuota: function() { 17 | this.quota = _.round(this.votes.length / (this.seats + 1), 2); 18 | this.voteweight = _.range(1, this.votes.length + 1, 0); 19 | }, 20 | 21 | displayVotes: function() { 22 | var model = this; 23 | this.outputstring += ''; 24 | _.each(this.votes, function(vote, idx) { 25 | model.outputstring += 'Vote ' + (idx + 1) + ':'; 26 | var colspan = model.names.length - vote.length; 27 | _.each(vote, function(name, idx2) { 28 | if (idx2 === 0) { 29 | model.outputstring += '' + name + ''; 30 | } else { 31 | model.outputstring += '' + name + ''; 32 | } 33 | }); 34 | 35 | if (colspan) { 36 | model.outputstring += ''; 37 | } 38 | 39 | if (model.seats > 1) { 40 | model.outputstring += 'vote-value = ' + _.round(model.voteweight[idx], 4) + ''; 41 | } 42 | }); 43 | this.outputstring += ''; 44 | }, 45 | 46 | createHeader: function() { 47 | if (this.seats > 1) { 48 | return 'Candidates: ' + this.names.length + ' Seats: ' + this.seats + ' Votes: ' + this.votes.length + ' Quota: ' + this.quota + '
'; 49 | } else { 50 | return 'Candidates: ' + this.names.length + ' Votes: ' + this.votes.length + '
'; 51 | } 52 | }, 53 | 54 | // Check for 'end' conditions otherwise count votes again 55 | anotherRound: function() { 56 | if (this.wincount == this.seats) { 57 | this.finishElection(); 58 | } else { 59 | this.outputstring += '

Round ' + (++this.roundnum) + ' votes'; 60 | this.displayVotes(); 61 | this.countVotes(); 62 | } 63 | }, 64 | 65 | countVotes: function() { 66 | var quotacount = 0; 67 | var model = this; 68 | this.votenum = _.range(0, this.names.length, 0); 69 | 70 | _.each(this.votes, function(vote, idx) { 71 | var choice = model.names.indexOf(vote[0]); 72 | model.votenum[choice] += model.voteweight[idx]; 73 | }); 74 | 75 | _.each(this.names, function(name, idx) { 76 | model.outputstring += name + ' = ' + _.round(model.votenum[idx], 4) + '
'; 77 | 78 | if (model.votenum[idx] > model.quota) { 79 | quotacount++; 80 | } 81 | }); 82 | 83 | this.buildDataForOutcome(quotacount); 84 | }, 85 | 86 | buildDataForOutcome: function(data) { 87 | if (data) { 88 | data = { 89 | math: 'max', 90 | class: 'elected', 91 | elect: true, 92 | text: { 93 | count: 'Most votes currently held', 94 | total: 'greatest number of', 95 | tie: 'says the first surplus to be re-allocated', 96 | result: 'has exceeded the quota and is elected. If there are seats remaining to be filled, the surplus will now be reallocated' 97 | } 98 | }; 99 | } else { 100 | data = { 101 | math: 'min', 102 | class: 'eliminated', 103 | text: { 104 | count: 'Fewest votes won', 105 | total: 'fewest', 106 | tie: 'loser', 107 | result: 'is eliminated' 108 | } 109 | }; 110 | } 111 | 112 | this.determineOutcome(data); 113 | }, 114 | 115 | // Show results for either winning candidate or losing candidate. 116 | determineOutcome: function(data) { 117 | // TODO: Eliminate every candidate with zero votes in the first round. 118 | // apex = votes needed to either be elected or be eliminated 119 | var apex = this.votenum.reduce(function(prev, current) { 120 | return current ? Math[data.math](prev, current) : prev; 121 | }, this.quota); 122 | var count = this.votenum.filter(function(num) { 123 | return num == apex; 124 | }).length; 125 | var chosen = this.votenum.indexOf(apex); 126 | 127 | this.outputstring += '
' + data.text.count + ' by a candidate = ' + _.round(apex, 4) + '.'; 128 | this.outputstring += '
Number of candidates with the ' + data.text.total + ' votes = ' + count + '.'; 129 | 130 | if (count > 1) { 131 | chosen = this.tieBreakMethod == 'weighted' ? this.breakTieWeighted : this.breakTieRandom(apex); 132 | this.outputstring += '
The random tiebreaker ' + data.text.tie + ' is ' + this.names[chosen] + '\'s.'; 133 | } 134 | 135 | this.outputstring += '
' + this.names[chosen] + ' ' + data.text.result + '.'; 136 | this.removeChosen(chosen, data.class, data.elect); 137 | }, 138 | 139 | // remove either the elected or eliminated candidates from votes 140 | removeChosen: function(chosen, className, elect) { 141 | if (elect) { 142 | this.elected[this.wincount++] = this.names[chosen]; 143 | } 144 | var model = this; 145 | _.each(this.votes, function(vote, index) { 146 | var found = vote.indexOf(model.names[chosen]); 147 | 148 | if (found !== -1) { 149 | if (found === 0) { 150 | if (elect) { 151 | model.voteweight[index] *= 1 - model.quota / model.votenum[chosen]; 152 | } 153 | vote.push('' + vote[found] + ''); 154 | } 155 | vote.splice(found, 1); 156 | } 157 | }); 158 | this.anotherRound(); 159 | }, 160 | 161 | // Analyses which candidates are marginally stronger for the purpose of breaking ties 162 | breakTieWeighted: function(value) { 163 | var model = this; 164 | var tieArray = []; 165 | var i; 166 | // length of longest vote array 167 | var voteSize = this.votes.reduce(function(voteSize, vote) { 168 | return Math.max(voteSize, vote.length); 169 | }, 0); 170 | var calculateValue = function(voteArr, idx) { 171 | var tie = _.find(tieArray, {name: voteArr[i]}); 172 | 173 | if (tie) { 174 | // 2nd place votes are exponentially greater than 3rd place votes etc. 175 | tie.value += model.voteweight[idx] / Math.pow(10, i); 176 | } 177 | }; 178 | // populate tieArray only with tie breakers 179 | this.votenum.map(function(val, idx) { 180 | if (val == value) { 181 | tieArray.push({ 182 | index: idx, 183 | name: model.names[idx], 184 | value: 0 185 | }); 186 | } 187 | }); 188 | 189 | for (i = 1; i < voteSize; i++) { 190 | this.votes.map(calculateValue); 191 | } 192 | // sort by ascending vote value 193 | tieArray.sort(function(a, b) { 194 | return a.value > b.value; 195 | }); 196 | 197 | return tieArray[0].index; 198 | }, 199 | 200 | // creative way to achieve repeatable randomizing 201 | breakTieRandom: function(value) { 202 | var model = this; 203 | var tieArray = []; 204 | var randomize = function(string) { 205 | string = model.votes.length + string.replace(/\W/g, '') + model.roundnum; 206 | // algorithm supplied by http://indiegamr.com/generate-repeatable-random-numbers-in-js/ 207 | return (parseInt(string, 36) * 9301 + 49297) % 233280; 208 | }; 209 | // populate tieArray only with tie breakers 210 | this.votenum.map(function(val, idx) { 211 | if (val == value) { 212 | tieArray.push({ 213 | index: idx, 214 | rand: randomize(model.names[idx] + idx) 215 | }); 216 | } 217 | }); 218 | 219 | // sort by ascending random value 220 | tieArray.sort(function(a, b) { 221 | return a.rand > b.rand; 222 | }); 223 | 224 | return tieArray[0].index; 225 | }, 226 | 227 | // Finish election and announce the winner(s). 228 | finishElection: function() { 229 | var model = this; 230 | this.outputstring += '

The election is complete and the elected candidates are'; 231 | _.each(this.elected, function(name) { 232 | model.outputstring += ' (' + name + ')'; 233 | }); 234 | this.outputstring += '.

'; 235 | } 236 | }; 237 | } 238 | ]); 239 | -------------------------------------------------------------------------------- /src/js/services/mc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moritz Company utility functions (for use anywhere) with mc.pluralize() 3 | **/ 4 | 5 | var mc = { 6 | pluralize: function pluralize(str) { 7 | return str.replace(/y$/, 'ie') + 's'; 8 | }, 9 | 10 | camelToTitle: function camelToTitle(str) { // convert camelCaseString to Title Case String 11 | return _.capitalize(str.replace(/([A-Z])/g, ' $1')).trim(); 12 | }, 13 | 14 | randomDigits: function randomDigits(min, max) { 15 | min = min === undefined ? 1 : min; 16 | max = max || 999; 17 | 18 | return Math.floor(Math.random() * (max - min + 1) + min); 19 | }, 20 | 21 | alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), 22 | 23 | isAngularObjectEqual: function isAngularObjectEqual(object1, object2) { 24 | return _.isEqual(_.omit(object1, '$$hashKey'), _.omit(object2, '$$hashKey')); 25 | }, 26 | 27 | expandArray: function expandArray(array, times) { // turns [1,2,3] into [1,2,3,1,2,3,1,2,3]; 28 | times = times || 3; // default number of times to expand it by 29 | 30 | var expandedArray = []; 31 | 32 | for (var i = 0; i < times; i++) { 33 | expandedArray = expandedArray.concat(angular.copy(array)); 34 | } 35 | 36 | return expandedArray; 37 | }, 38 | 39 | calculateAge: function calculateAge(dateOfBirth) { 40 | var age; 41 | 42 | if (dateOfBirth) { 43 | var year = Number(dateOfBirth.substr(0, 4)); 44 | var month = Number(dateOfBirth.substr(5, 2)) - 1; 45 | var day = Number(dateOfBirth.substr(8, 2)); 46 | var today = new Date(); 47 | age = today.getFullYear() - year; 48 | 49 | if (today.getMonth() < month || (today.getMonth() == month && today.getDate() < day)) { 50 | age--; 51 | } 52 | } 53 | 54 | return age || 0; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/less/all.less: -------------------------------------------------------------------------------- 1 | /*** Main stylesheets ***/ 2 | @import "main-theme"; 3 | @import (less) "../../lib/jquery-ui/themes/smoothness/jquery-ui.css"; 4 | @import "../../lib/fontawesome/less/font-awesome"; 5 | @import "common"; 6 | @import "jquery-ui"; 7 | @import "elections"; 8 | @import "pages"; 9 | @import "fair-vote"; -------------------------------------------------------------------------------- /src/less/common.less: -------------------------------------------------------------------------------- 1 | /***** Common CSS ******/ 2 | 3 | .pos-rel { 4 | position: relative; 5 | } 6 | .centered { 7 | margin: auto; 8 | } 9 | .pointer { 10 | cursor: pointer; 11 | } 12 | .upper { 13 | text-transform: uppercase; 14 | } 15 | [hidden] { 16 | display: none; 17 | } 18 | .btn { 19 | margin-top: 2px; 20 | .input-group-btn & { 21 | margin-top: 0; 22 | } 23 | } 24 | .btn-icon { 25 | margin-left: 6px; 26 | font-size: 115%; 27 | } 28 | .alert-flip { 29 | margin-bottom: 0px; 30 | margin-top: 20px; 31 | } 32 | .block { 33 | display: block; 34 | } 35 | .inline-block, 36 | .inline-block.collapse.in { 37 | display: inline-block; 38 | } 39 | .inline, 40 | .inline.collapse.in { 41 | display: inline; 42 | } 43 | #performance-gauge {width: 85%;} 44 | 45 | // add Bootstrap ms size 46 | .col-ms-1, 47 | .col-ms-2, 48 | .col-ms-3, 49 | .col-ms-4, 50 | .col-ms-5, 51 | .col-ms-6, 52 | .col-ms-7, 53 | .col-ms-8, 54 | .col-ms-9, 55 | .col-ms-10, 56 | .col-ms-11, 57 | .col-ms-12 { 58 | position: relative; 59 | min-height: 1px; 60 | padding-left: 15px; 61 | padding-right: 15px; } 62 | 63 | @media (min-width: @screen-xs) and (max-width: @screen-xs-max) { 64 | .visible-xs-only { 65 | display: none !important; 66 | } 67 | .visible-ms { 68 | display: block; 69 | } 70 | .visible-ms-block { 71 | display: block !important; 72 | } 73 | .visible-ms-inline-block { 74 | display: inline-block !important; 75 | } 76 | .visible-ms-inline { 77 | display: inline !important; 78 | } 79 | .hidden-ms { 80 | display: none !important; 81 | } 82 | .col-ms-1, 83 | .col-ms-2, 84 | .col-ms-3, 85 | .col-ms-4, 86 | .col-ms-5, 87 | .col-ms-6, 88 | .col-ms-7, 89 | .col-ms-8, 90 | .col-ms-9, 91 | .col-ms-10, 92 | .col-ms-11 { 93 | float: left; } 94 | 95 | .col-ms-1 { 96 | width: 8.33333%; } 97 | 98 | .col-ms-2 { 99 | width: 16.66667%; } 100 | 101 | .col-ms-3 { 102 | width: 25%; } 103 | 104 | .col-ms-4 { 105 | width: 33.33333%; } 106 | 107 | .col-ms-5 { 108 | width: 41.66667%; } 109 | 110 | .col-ms-6 { 111 | width: 50%; } 112 | 113 | .col-ms-7 { 114 | width: 58.33333%; } 115 | 116 | .col-ms-8 { 117 | width: 66.66667%; } 118 | 119 | .col-ms-9 { 120 | width: 75%; } 121 | 122 | .col-ms-10 { 123 | width: 83.33333%; } 124 | 125 | .col-ms-11 { 126 | width: 91.66667%; } 127 | 128 | .col-ms-12 { 129 | width: 100%; } 130 | 131 | .col-ms-push-1 { 132 | left: 8.33333%; } 133 | 134 | .col-ms-push-2 { 135 | left: 16.66667%; } 136 | 137 | .col-ms-push-3 { 138 | left: 25%; } 139 | 140 | .col-ms-push-4 { 141 | left: 33.33333%; } 142 | 143 | .col-ms-push-5 { 144 | left: 41.66667%; } 145 | 146 | .col-ms-push-6 { 147 | left: 50%; } 148 | 149 | .col-ms-push-7 { 150 | left: 58.33333%; } 151 | 152 | .col-ms-push-8 { 153 | left: 66.66667%; } 154 | 155 | .col-ms-push-9 { 156 | left: 75%; } 157 | 158 | .col-ms-push-10 { 159 | left: 83.33333%; } 160 | 161 | .col-ms-push-11 { 162 | left: 91.66667%; } 163 | 164 | .col-ms-pull-1 { 165 | right: 8.33333%; } 166 | 167 | .col-ms-pull-2 { 168 | right: 16.66667%; } 169 | 170 | .col-ms-pull-3 { 171 | right: 25%; } 172 | 173 | .col-ms-pull-4 { 174 | right: 33.33333%; } 175 | 176 | .col-ms-pull-5 { 177 | right: 41.66667%; } 178 | 179 | .col-ms-pull-6 { 180 | right: 50%; } 181 | 182 | .col-ms-pull-7 { 183 | right: 58.33333%; } 184 | 185 | .col-ms-pull-8 { 186 | right: 66.66667%; } 187 | 188 | .col-ms-pull-9 { 189 | right: 75%; } 190 | 191 | .col-ms-pull-10 { 192 | right: 83.33333%; } 193 | 194 | .col-ms-pull-11 { 195 | right: 91.66667%; } 196 | 197 | .col-ms-offset-1 { 198 | margin-left: 8.33333%; } 199 | 200 | .col-ms-offset-2 { 201 | margin-left: 16.66667%; } 202 | 203 | .col-ms-offset-3 { 204 | margin-left: 25%; } 205 | 206 | .col-ms-offset-4 { 207 | margin-left: 33.33333%; } 208 | 209 | .col-ms-offset-5 { 210 | margin-left: 41.66667%; } 211 | 212 | .col-ms-offset-6 { 213 | margin-left: 50%; } 214 | 215 | .col-ms-offset-7 { 216 | margin-left: 58.33333%; } 217 | 218 | .col-ms-offset-8 { 219 | margin-left: 66.66667%; } 220 | 221 | .col-ms-offset-9 { 222 | margin-left: 75%; } 223 | 224 | .col-ms-offset-10 { 225 | margin-left: 83.33333%; } 226 | 227 | .col-ms-offset-11 { 228 | margin-left: 91.66667%; } 229 | } 230 | 231 | @media (min-width: @screen-xs) and (max-width: @screen-xs-max) { 232 | .container { 233 | max-width: @screen-sm; 234 | } 235 | 236 | .form-horizontal .form-group .control-label {text-align:right;} 237 | } -------------------------------------------------------------------------------- /src/less/elections.less: -------------------------------------------------------------------------------- 1 | .elected { 2 | color: green; 3 | font-weight: bold; 4 | } 5 | .eliminated { 6 | color: red; 7 | text-decoration: line-through; 8 | } 9 | .next-vote { 10 | color: darkred; 11 | font-style: italic; 12 | font-weight: bold; 13 | } 14 | body { 15 | background-color: rgba(255, 255, 255, 0); 16 | } -------------------------------------------------------------------------------- /src/less/jquery-ui.less: -------------------------------------------------------------------------------- 1 | #sortable { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | cursor: move; 6 | position: relative; 7 | li { 8 | margin: 0 3px 3px 3px; 9 | padding: 5px 20px 5px 5px; 10 | font-size: 130%; 11 | border: solid 1px #709670; 12 | border-radius: 5px; 13 | background: rgba(245, 245, 245, 0.75); 14 | span { 15 | position: absolute; 16 | margin-left: -1.3em; 17 | } 18 | i { 19 | position: absolute; 20 | right: 5px; 21 | cursor: pointer; 22 | padding: 4px; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/less/main-theme.less: -------------------------------------------------------------------------------- 1 | /* Import the Base Bootstrap Files */ 2 | @import "../../lib/bootstrap/less/bootstrap.less"; 3 | 4 | // Variables 5 | 6 | @gray-darker: lighten(#000, 13.5%); 7 | @gray-dark: lighten(#000, 20%); 8 | @gray: lighten(#000, 33.5%); 9 | @gray-light: lighten(#000, 60%); 10 | @gray-lighter: lighten(#000, 93.5%); 11 | @gray-lightest: lighten(#000, 97.25%); 12 | @brand-primary: #428bca; 13 | @brand-success: #5cb85c; 14 | @brand-info: #5bc0de; 15 | @brand-warning: #f0ad4e; 16 | @brand-danger: #d9534f; 17 | 18 | // Global Styles 19 | 20 | body { 21 | background-color: @gray-lightest; 22 | } 23 | 24 | // Wrappers 25 | 26 | #wrapper { 27 | width: 100%; 28 | } 29 | 30 | #page-wrapper { 31 | padding: 0 15px; 32 | min-height: 568px; 33 | background-color: white; 34 | } 35 | 36 | @media(min-width:768px) { 37 | #page-wrapper { 38 | position: inherit; 39 | margin: 0 0 0 250px; 40 | padding: 0 30px; 41 | border-left: 1px solid darken(@gray-lightest, 6.5%); 42 | } 43 | } 44 | 45 | // Navigation 46 | 47 | // --Topbar 48 | 49 | .navbar-top-links { 50 | margin-right: 0; 51 | } 52 | 53 | .navbar-top-links li { 54 | display: inline-block; 55 | } 56 | 57 | .navbar-top-links li:last-child { 58 | margin-right: 15px; 59 | } 60 | 61 | .navbar-top-links li a { 62 | padding: 15px; 63 | min-height: 50px; 64 | } 65 | 66 | .navbar-top-links .dropdown-menu li { 67 | display: block; 68 | } 69 | 70 | .navbar-top-links .dropdown-menu li:last-child { 71 | margin-right: 0; 72 | } 73 | 74 | .navbar-top-links .dropdown-menu li a { 75 | padding: 3px 20px; 76 | min-height: 0; 77 | } 78 | 79 | .navbar-top-links .dropdown-menu li a div { 80 | white-space: normal; 81 | } 82 | 83 | .navbar-top-links .dropdown-messages, 84 | .navbar-top-links .dropdown-tasks, 85 | .navbar-top-links .dropdown-alerts { 86 | width: 310px; 87 | min-width: 0; 88 | } 89 | 90 | .navbar-top-links .dropdown-messages { 91 | margin-left: 5px; 92 | } 93 | 94 | .navbar-top-links .dropdown-tasks { 95 | margin-left: -59px; 96 | } 97 | 98 | .navbar-top-links .dropdown-alerts { 99 | margin-left: -123px; 100 | } 101 | 102 | .navbar-top-links .dropdown-user { 103 | right: 0; 104 | left: auto; 105 | } 106 | 107 | // --Sidebar 108 | 109 | .sidebar { 110 | .sidebar-nav.navbar-collapse { 111 | padding-left: 0; 112 | padding-right: 0; 113 | } 114 | } 115 | 116 | .sidebar .sidebar-search { 117 | padding: 15px; 118 | } 119 | 120 | .sidebar ul li { 121 | border-bottom: 1px solid darken(@gray-lightest, 6.5%); 122 | a { 123 | &.active { 124 | background-color: @gray-lighter; 125 | } 126 | } 127 | } 128 | 129 | .sidebar .arrow { 130 | float: right; 131 | } 132 | 133 | .sidebar .fa.arrow:before { 134 | content: "\f104"; 135 | } 136 | 137 | .sidebar .active > a > .fa.arrow:before { 138 | content: "\f107"; 139 | } 140 | 141 | .sidebar .nav-second-level li, 142 | .sidebar .nav-third-level li { 143 | border-bottom: none !important; 144 | } 145 | 146 | .sidebar .nav-second-level li a { 147 | padding-left: 37px; 148 | } 149 | 150 | .sidebar .nav-third-level li a { 151 | padding-left: 52px; 152 | } 153 | 154 | @media(min-width:768px) { 155 | .sidebar { 156 | z-index: 1; 157 | position: absolute; 158 | width: 250px; 159 | margin-top: 51px; 160 | } 161 | 162 | .navbar-top-links .dropdown-messages, 163 | .navbar-top-links .dropdown-tasks, 164 | .navbar-top-links .dropdown-alerts { 165 | margin-left: auto; 166 | } 167 | } 168 | 169 | // Buttons 170 | 171 | .btn-outline { 172 | color: inherit; 173 | background-color: transparent; 174 | transition: all .5s; 175 | } 176 | 177 | .btn-primary.btn-outline { 178 | color: @brand-primary; 179 | } 180 | 181 | .btn-success.btn-outline { 182 | color: @brand-success; 183 | } 184 | 185 | .btn-info.btn-outline { 186 | color: @brand-info; 187 | } 188 | 189 | .btn-warning.btn-outline { 190 | color: @brand-warning; 191 | } 192 | 193 | .btn-danger.btn-outline { 194 | color: @brand-danger; 195 | } 196 | 197 | .btn-primary.btn-outline:hover, 198 | .btn-success.btn-outline:hover, 199 | .btn-info.btn-outline:hover, 200 | .btn-warning.btn-outline:hover, 201 | .btn-danger.btn-outline:hover { 202 | color: white; 203 | } 204 | 205 | // Chat Widget 206 | 207 | .chat { 208 | margin: 0; 209 | padding: 0; 210 | list-style: none; 211 | } 212 | 213 | .chat li { 214 | margin-bottom: 10px; 215 | padding-bottom: 5px; 216 | border-bottom: 1px dotted @gray-light; 217 | } 218 | 219 | .chat li.left .chat-body { 220 | margin-left: 60px; 221 | } 222 | 223 | .chat li.right .chat-body { 224 | margin-right: 60px; 225 | } 226 | 227 | .chat li .chat-body p { 228 | margin: 0; 229 | } 230 | 231 | .panel .slidedown .glyphicon, 232 | .chat .glyphicon { 233 | margin-right: 5px; 234 | } 235 | 236 | .chat-panel .panel-body { 237 | height: 350px; 238 | overflow-y: scroll; 239 | } 240 | 241 | // Login Page 242 | 243 | .login-panel { 244 | margin-top: 25%; 245 | } 246 | 247 | // Flot Charts Containers 248 | 249 | .flot-chart { 250 | display: block; 251 | height: 400px; 252 | } 253 | 254 | .flot-chart-content { 255 | width: 100%; 256 | height: 100%; 257 | } 258 | 259 | // DataTables Overrides 260 | 261 | table.dataTable thead .sorting, 262 | table.dataTable thead .sorting_asc, 263 | table.dataTable thead .sorting_desc, 264 | table.dataTable thead .sorting_asc_disabled, 265 | table.dataTable thead .sorting_desc_disabled { 266 | background: transparent; 267 | } 268 | 269 | table.dataTable thead .sorting_asc:after { 270 | content: "\f0de"; 271 | float: right; 272 | font-family: fontawesome; 273 | } 274 | 275 | table.dataTable thead .sorting_desc:after { 276 | content: "\f0dd"; 277 | float: right; 278 | font-family: fontawesome; 279 | } 280 | 281 | table.dataTable thead .sorting:after { 282 | content: "\f0dc"; 283 | float: right; 284 | font-family: fontawesome; 285 | color: rgba(50,50,50,.5); 286 | } 287 | 288 | // Circle Buttons 289 | 290 | .btn-circle { 291 | width: 30px; 292 | height: 30px; 293 | padding: 6px 0; 294 | border-radius: 15px; 295 | text-align: center; 296 | font-size: 12px; 297 | line-height: 1.428571429; 298 | } 299 | 300 | .btn-circle.btn-lg { 301 | width: 50px; 302 | height: 50px; 303 | padding: 10px 16px; 304 | border-radius: 25px; 305 | font-size: 18px; 306 | line-height: 1.33; 307 | } 308 | 309 | .btn-circle.btn-xl { 310 | width: 70px; 311 | height: 70px; 312 | padding: 10px 16px; 313 | border-radius: 35px; 314 | font-size: 24px; 315 | line-height: 1.33; 316 | } 317 | 318 | // Grid Demo Elements 319 | 320 | .show-grid [class^="col-"] { 321 | padding-top: 10px; 322 | padding-bottom: 10px; 323 | border: 1px solid #ddd; 324 | background-color: #eee !important; 325 | } 326 | 327 | .show-grid { 328 | margin: 15px 0; 329 | } 330 | 331 | // Custom Colored Panels 332 | 333 | .huge { 334 | font-size: 40px; 335 | } 336 | 337 | .panel-green { 338 | border-color: @brand-success; 339 | .panel-heading { 340 | border-color: @brand-success; 341 | color: white; 342 | background-color: @brand-success; 343 | } 344 | a { 345 | color: @brand-success; 346 | &:hover { 347 | color: darken(@brand-success, 15%); 348 | } 349 | } 350 | } 351 | 352 | .panel-red { 353 | border-color: @brand-danger; 354 | .panel-heading { 355 | border-color: @brand-danger; 356 | color: white; 357 | background-color: @brand-danger; 358 | } 359 | a { 360 | color: @brand-danger; 361 | &:hover { 362 | color: darken(@brand-danger, 15%); 363 | } 364 | } 365 | } 366 | 367 | .panel-yellow { 368 | border-color: @brand-warning; 369 | .panel-heading { 370 | border-color: @brand-warning; 371 | color: white; 372 | background-color: @brand-warning; 373 | } 374 | a { 375 | color: @brand-warning; 376 | &:hover { 377 | color: darken(@brand-warning, 15%); 378 | } 379 | } 380 | } -------------------------------------------------------------------------------- /src/less/pages.less: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #ebebeb !important; 3 | } 4 | 5 | .navbar { 6 | background-color: #553285; 7 | } 8 | 9 | .fluid-video-wrapper { 10 | padding: 56.25% 0 0; 11 | width: 100%; 12 | position: relative; 13 | iframe { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | } 21 | 22 | .vote-button { 23 | max-height: 300px; 24 | margin: auto; 25 | } 26 | 27 | table.ballots { 28 | .fa { 29 | padding: 3px; 30 | color: inherit; 31 | &:hover { 32 | color: inherit; 33 | text-decoration: inherit; 34 | } 35 | } 36 | } --------------------------------------------------------------------------------